1
2
3
4 """
5 Copyright (c) 2010 The Echo Nest. All rights reserved.
6 Created by Tyler Williams on 2010-04-25.
7
8 Utility functions to support the Echo Nest web API interface.
9 """
10 import urllib
11 import urllib2
12 import httplib
13 import config
14 import logging
15 import socket
16 import re
17 import time
18 import os
19 import subprocess
20 import traceback
21 from types import StringType, UnicodeType
22
23 try:
24 import json
25 except ImportError:
26 import simplejson as json
27
28 logging.basicConfig(level=logging.INFO)
29 logger = logging.getLogger(__name__)
30 TYPENAMES = (
31 ('AR', 'artist'),
32 ('SO', 'song'),
33 ('RE', 'release'),
34 ('TR', 'track'),
35 ('PE', 'person'),
36 ('DE', 'device'),
37 ('LI', 'listener'),
38 ('ED', 'editor'),
39 ('TW', 'tweditor'),
40 ('CA', 'catalog'),
41 )
42 foreign_regex = re.compile(r'^.+?:(%s):([^^]+)\^?([0-9\.]+)?' % r'|'.join(n[1] for n in TYPENAMES))
43 short_regex = re.compile(r'^((%s)[0-9A-Z]{16})\^?([0-9\.]+)?' % r'|'.join(n[0] for n in TYPENAMES))
44 long_regex = re.compile(r'music://id.echonest.com/.+?/(%s)/(%s)[0-9A-Z]{16}\^?([0-9\.]+)?' % (r'|'.join(n[0] for n in TYPENAMES), r'|'.join(n[0] for n in TYPENAMES)))
45 headers = [('User-Agent', 'Pyechonest %s' % (config.__version__,))]
46
53
56 code = response.code
57 if config.TRACE_API_CALLS:
58 logger.info("took %2.2fs: (%i)" % (time.time()-request.start_time,code))
59 if code in [200, 400, 401, 403, 500]:
60 return response
61 else:
62 urllib2.HTTPErrorProcessor.http_response(self, request, response)
63
64 opener = urllib2.build_opener(MyBaseHandler(), MyErrorProcessor())
65 opener.addheaders = headers
66
68 """
69 Parent exception class. Catches API and URL/HTTP errors.
70 """
71 - def __init__(self, code, message, headers):
82
84 """
85 API Specific Errors.
86 """
87 - def __init__(self, code, message, headers):
90
92 """
93 URL and HTTP errors.
94 """
98
100 if hasattr(raw_json,'headers'):
101 headers = raw_json.headers
102 else:
103 headers = {'Headers':'No Headers'}
104 raw_json = raw_json.read()
105 try:
106 response_dict = json.loads(raw_json)
107 status_dict = response_dict['response']['status']
108 code = int(status_dict['code'])
109 message = status_dict['message']
110 if (code != 0):
111
112 raise EchoNestAPIError(code, message, headers)
113 del response_dict['response']['status']
114 return response_dict
115 except ValueError:
116 logger.debug(traceback.format_exc())
117 raise EchoNestAPIError(-1, "Unknown error.", headers)
118
119
120
122 if type(s) is StringType:
123 for args in ((encoding,), ('utf-8',), ('latin-1',), ('ascii', 'replace')):
124 try:
125 s = s.decode(*args)
126 break
127 except UnicodeDecodeError:
128 continue
129 if type(s) is not UnicodeType:
130 raise ValueError, "%s is not a string at all." % s
131 return s
132
135
136 -def codegen(filename, start=0, duration=30):
137
138 cmd = config.CODEGEN_BINARY_OVERRIDE
139 if not cmd:
140
141 if hasattr(os, 'uname'):
142 if(os.uname()[0] == "Darwin"):
143 cmd = "codegen.Darwin"
144 else:
145 cmd = 'codegen.'+os.uname()[0]+'-'+os.uname()[4]
146 else:
147 cmd = "codegen.windows.exe"
148
149 if not os.path.exists(cmd):
150 raise Exception("Codegen binary not found.")
151
152 command = cmd + " \"" + filename + "\" "
153 if start >= 0:
154 command = command + str(start) + " "
155 if duration >= 0:
156 command = command + str(duration)
157
158 p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
159 (json_block, errs) = p.communicate()
160 json_block = reallyUTF8(json_block)
161
162 try:
163 return json.loads(json_block)
164 except ValueError:
165 logger.debug("No JSON object came out of codegen: error was %s" % (errs))
166 return None
167
168
169 -def callm(method, param_dict, POST=False, socket_timeout=None, data=None):
170 """
171 Call the api!
172 Param_dict is a *regular* *python* *dictionary* so if you want to have multi-valued params
173 put them in a list.
174
175 ** note, if we require 2.6, we can get rid of this timeout munging.
176 """
177 try:
178 param_dict['api_key'] = config.ECHO_NEST_API_KEY
179 param_list = []
180 if not socket_timeout:
181 socket_timeout = config.CALL_TIMEOUT
182
183 for key,val in param_dict.iteritems():
184 if isinstance(val, list):
185 param_list.extend( [(key,subval) for subval in val] )
186 elif val is not None:
187 if isinstance(val, unicode):
188 val = val.encode('utf-8')
189 param_list.append( (key,val) )
190
191 params = urllib.urlencode(param_list)
192
193 orig_timeout = socket.getdefaulttimeout()
194 socket.setdefaulttimeout(socket_timeout)
195
196 if(POST):
197 if (not method == 'track/upload') or ((method == 'track/upload') and 'url' in param_dict):
198 """
199 this is a normal POST call
200 """
201 url = 'http://%s/%s/%s/%s' % (config.API_HOST, config.API_SELECTOR,
202 config.API_VERSION, method)
203
204 if data is None:
205 data = ''
206 data = urllib.urlencode(data)
207 data = "&".join([data, params])
208
209 f = opener.open(url, data=data)
210 else:
211 """
212 upload with a local file is special, as the body of the request is the content of the file,
213 and the other parameters stay on the URL
214 """
215 url = '/%s/%s/%s?%s' % (config.API_SELECTOR, config.API_VERSION,
216 method, params)
217
218 if ':' in config.API_HOST:
219 host, port = config.API_HOST.split(':')
220 else:
221 host = config.API_HOST
222 port = 80
223
224 if config.TRACE_API_CALLS:
225 logger.info("%s/%s" % (host+':'+str(port), url,))
226 conn = httplib.HTTPConnection(host, port = port)
227 conn.request('POST', url, body = data, headers = dict([('Content-Type', 'application/octet-stream')]+headers))
228 f = conn.getresponse()
229
230 else:
231 """
232 just a normal GET call
233 """
234 url = 'http://%s/%s/%s/%s?%s' % (config.API_HOST, config.API_SELECTOR, config.API_VERSION,
235 method, params)
236
237 f = opener.open(url)
238
239 socket.setdefaulttimeout(orig_timeout)
240
241
242 response_dict = get_successful_response(f)
243 return response_dict
244
245 except IOError, e:
246 if hasattr(e, 'reason'):
247 print 'Failed to reach the Echo Nest server.'
248 print 'Reason: ', e.reason
249 raise EchoNestIOError(error=e.reason)
250 elif hasattr(e, 'code'):
251 print 'Echo Nest server couldn\'t fulfill the request.'
252 print 'Error code: ', e.code
253 raise EchoNestIOError(code=e.code)
254 else:
255 raise
256
257 -def oauthgetm(method, param_dict, socket_timeout=None):
258 try:
259 import oauth2
260 except ImportError:
261 raise Exception("You must install the python-oauth2 library to use this method.")
262
263 """
264 Call the api! With Oauth!
265 Param_dict is a *regular* *python* *dictionary* so if you want to have multi-valued params
266 put them in a list.
267
268 ** note, if we require 2.6, we can get rid of this timeout munging.
269 """
270 def build_request(url):
271 params = {
272 'oauth_version': "1.0",
273 'oauth_nonce': oauth2.generate_nonce(),
274 'oauth_timestamp': int(time.time())
275 }
276 consumer = oauth2.Consumer(key=config.ECHO_NEST_CONSUMER_KEY, secret=config.ECHO_NEST_SHARED_SECRET)
277 params['oauth_consumer_key'] = config.ECHO_NEST_CONSUMER_KEY
278
279 req = oauth2.Request(method='GET', url=url, parameters=params)
280 signature_method = oauth2.SignatureMethod_HMAC_SHA1()
281 req.sign_request(signature_method, consumer, None)
282 return req
283
284 param_dict['api_key'] = config.ECHO_NEST_API_KEY
285 param_list = []
286 if not socket_timeout:
287 socket_timeout = config.CALL_TIMEOUT
288
289 for key,val in param_dict.iteritems():
290 if isinstance(val, list):
291 param_list.extend( [(key,subval) for subval in val] )
292 elif val is not None:
293 if isinstance(val, unicode):
294 val = val.encode('utf-8')
295 param_list.append( (key,val) )
296
297 params = urllib.urlencode(param_list)
298
299 orig_timeout = socket.getdefaulttimeout()
300 socket.setdefaulttimeout(socket_timeout)
301 """
302 just a normal GET call
303 """
304 url = 'http://%s/%s/%s/%s?%s' % (config.API_HOST, config.API_SELECTOR, config.API_VERSION,
305 method, params)
306 req = build_request(url)
307 f = opener.open(req.to_url())
308
309 socket.setdefaulttimeout(orig_timeout)
310
311
312 response_dict = get_successful_response(f)
313 return response_dict
314
315
316 -def postChunked(host, selector, fields, files):
317 """
318 Attempt to replace postMultipart() with nearly-identical interface.
319 (The files tuple no longer requires the filename, and we only return
320 the response body.)
321 Uses the urllib2_file.py originally from
322 http://fabien.seisen.org which was also drawn heavily from
323 http://code.activestate.com/recipes/146306/ .
324
325 This urllib2_file.py is more desirable because of the chunked
326 uploading from a file pointer (no need to read entire file into
327 memory) and the ability to work from behind a proxy (due to its
328 basis on urllib2).
329 """
330 params = urllib.urlencode(fields)
331 url = 'http://%s%s?%s' % (host, selector, params)
332 u = urllib2.urlopen(url, files)
333 result = u.read()
334 [fp.close() for (key, fp) in files]
335 return result
336
337
339
340 assert(isinstance(x,dict))
341 return dict((str(k), v) for (k,v) in x.iteritems())
342