PageRenderTime 86ms CodeModel.GetById 21ms app.highlight 26ms RepoModel.GetById 17ms app.codeStats 12ms

/src/pyechonest/pyechonest/util.py

http://echo-nest-remix.googlecode.com/
Python | 241 lines | 193 code | 23 blank | 25 comment | 41 complexity | d3e4b750e36ee8d74d7d00090fb7ebb6 MD5 | raw file
  1#!/usr/bin/env python
  2# encoding: utf-8
  3
  4"""
  5Copyright (c) 2010 The Echo Nest. All rights reserved.
  6Created by Tyler Williams on 2010-04-25.
  7
  8Utility functions to support the Echo Nest web API interface.
  9"""
 10import urllib
 11import urllib2
 12import httplib
 13import config
 14import logging
 15import socket
 16import re
 17import time
 18import os
 19import subprocess
 20import traceback
 21from types import StringType, UnicodeType
 22
 23try:
 24    import json
 25except ImportError:
 26    import simplejson as json
 27
 28logging.basicConfig(level=logging.INFO)
 29logger = logging.getLogger(__name__)
 30TYPENAMES = (
 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)
 42foreign_regex = re.compile(r'^.+?:(%s):([^^]+)\^?([0-9\.]+)?' % r'|'.join(n[1] for n in TYPENAMES))
 43short_regex = re.compile(r'^((%s)[0-9A-Z]{16})\^?([0-9\.]+)?' % r'|'.join(n[0] for n in TYPENAMES))
 44long_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)))
 45headers = [('User-Agent', 'Pyechonest %s' % (config.__version__,))]
 46
 47class MyBaseHandler(urllib2.BaseHandler):
 48    def default_open(self, request):
 49        if config.TRACE_API_CALLS:
 50            logger.info("%s" % (request.get_full_url(),))
 51        request.start_time = time.time()
 52        return None
 53        
 54class MyErrorProcessor(urllib2.HTTPErrorProcessor):
 55    def http_response(self, request, response):
 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, 403, 500]:
 60            return response
 61        else:
 62            urllib2.HTTPErrorProcessor.http_response(self, request, response)
 63
 64opener = urllib2.build_opener(MyBaseHandler(), MyErrorProcessor())
 65opener.addheaders = headers
 66
 67class EchoNestAPIError(Exception):
 68    """
 69    Generic API errors. 
 70    """
 71    def __init__(self, code, message):
 72        self.args = ('Echo Nest API Error %d: %s' % (code, message),)
 73
 74def get_successful_response(raw_json):
 75    try:
 76        response_dict = json.loads(raw_json)
 77        status_dict = response_dict['response']['status']
 78        code = int(status_dict['code'])
 79        message = status_dict['message']
 80        if (code != 0):
 81            # do some cute exception handling
 82            raise EchoNestAPIError(code, message)
 83        del response_dict['response']['status']
 84        return response_dict
 85    except ValueError:
 86        logger.debug(traceback.format_exc())
 87        raise EchoNestAPIError(-1, "Unknown error.")
 88
 89
 90# These two functions are to deal with the unknown encoded output of codegen (varies by platform and ID3 tag)
 91def reallyunicode(s, encoding="utf-8"):
 92    if type(s) is StringType:
 93        for args in ((encoding,), ('utf-8',), ('latin-1',), ('ascii', 'replace')):
 94            try:
 95                s = s.decode(*args)
 96                break
 97            except UnicodeDecodeError:
 98                continue
 99    if type(s) is not UnicodeType:
100        raise ValueError, "%s is not a string at all." % s
101    return s
102
103def reallyUTF8(s):
104    return reallyunicode(s).encode("utf-8")
105
106def codegen(filename, start=0, duration=30):
107    # Run codegen on the file and return the json. If start or duration is -1 ignore them.
108    cmd = config.CODEGEN_BINARY_OVERRIDE
109    if not cmd:
110        # Is this is posix platform, or is it windows?
111        if hasattr(os, 'uname'):
112            if(os.uname()[0] == "Darwin"):
113                cmd = "codegen.Darwin"
114            else:
115                cmd = 'codegen.'+os.uname()[0]+'-'+os.uname()[4]
116        else:
117            cmd = "codegen.windows.exe"
118
119    if not os.path.exists(cmd):
120        raise Exception("Codegen binary not found.")
121
122    command = cmd + " \"" + filename + "\" " 
123    if start >= 0:
124        command = command + str(start) + " "
125    if duration >= 0:
126        command = command + str(duration)
127        
128    p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
129    (json_block, errs) = p.communicate()
130    json_block = reallyUTF8(json_block)
131
132    try:
133        return json.loads(json_block)
134    except ValueError:
135        logger.debug("No JSON object came out of codegen: error was %s" % (errs))
136        return None
137
138
139def callm(method, param_dict, POST=False, socket_timeout=None, data=None):
140    """
141    Call the api! 
142    Param_dict is a *regular* *python* *dictionary* so if you want to have multi-valued params
143    put them in a list.
144    
145    ** note, if we require 2.6, we can get rid of this timeout munging.
146    """
147    param_dict['api_key'] = config.ECHO_NEST_API_KEY
148    param_list = []
149    if not socket_timeout:
150        socket_timeout = config.CALL_TIMEOUT
151    
152    for key,val in param_dict.iteritems():
153        if isinstance(val, list):
154            param_list.extend( [(key,subval) for subval in val] )
155        elif val is not None:
156            if isinstance(val, unicode):
157                val = val.encode('utf-8')
158            param_list.append( (key,val) )
159
160    params = urllib.urlencode(param_list)
161    
162    orig_timeout = socket.getdefaulttimeout()
163    socket.setdefaulttimeout(socket_timeout)
164
165    if(POST):
166        if (not method == 'track/upload') or ((method == 'track/upload') and 'url' in param_dict):
167            """
168            this is a normal POST call
169            """
170            url = 'http://%s/%s/%s/%s' % (config.API_HOST, config.API_SELECTOR, 
171                                        config.API_VERSION, method)
172            
173            if data is None:
174                data = ''
175            data = urllib.urlencode(data)
176            data = "&".join([data, params])
177
178            f = opener.open(url, data=data)
179        else:
180            """
181            upload with a local file is special, as the body of the request is the content of the file,
182            and the other parameters stay on the URL
183            """
184            url = '/%s/%s/%s?%s' % (config.API_SELECTOR, config.API_VERSION, 
185                                        method, params)
186
187            if ':' in config.API_HOST:
188                host, port = config.API_HOST.split(':')
189            else:
190                host = config.API_HOST
191                port = 80
192                
193            if config.TRACE_API_CALLS:
194                logger.info("%s/%s" % (host+':'+str(port), url,))
195            conn = httplib.HTTPConnection(host, port = port)
196            conn.request('POST', url, body = data, headers = dict([('Content-Type', 'application/octet-stream')]+headers))
197            f = conn.getresponse()
198
199    else:
200        """
201        just a normal GET call
202        """
203        url = 'http://%s/%s/%s/%s?%s' % (config.API_HOST, config.API_SELECTOR, config.API_VERSION, 
204                                        method, params)
205
206        f = opener.open(url)
207            
208    socket.setdefaulttimeout(orig_timeout)
209    
210    # try/except
211    response_dict = get_successful_response(f.read())
212    return response_dict
213
214
215def postChunked(host, selector, fields, files):
216    """
217    Attempt to replace postMultipart() with nearly-identical interface.
218    (The files tuple no longer requires the filename, and we only return
219    the response body.) 
220    Uses the urllib2_file.py originally from 
221    http://fabien.seisen.org which was also drawn heavily from 
222    http://code.activestate.com/recipes/146306/ .
223    
224    This urllib2_file.py is more desirable because of the chunked 
225    uploading from a file pointer (no need to read entire file into 
226    memory) and the ability to work from behind a proxy (due to its 
227    basis on urllib2).
228    """
229    params = urllib.urlencode(fields)
230    url = 'http://%s%s?%s' % (host, selector, params)
231    u = urllib2.urlopen(url, files)
232    result = u.read()
233    [fp.close() for (key, fp) in files]
234    return result
235
236
237def fix(x):
238    # we need this to fix up all the dict keys to be strings, not unicode objects
239    assert(isinstance(x,dict))
240    return dict((str(k), v) for (k,v) in x.iteritems())
241