# Tweepy # Copyright 2009-2010 Joshua Roesslein # See LICENSE for details. import httplib import urllib import time import re from StringIO import StringIO import gzip from tweepy.error import TweepError from tweepy.utils import convert_to_utf8_str from tweepy.models import Model re_path_template = re.compile('{\w+}') def bind_api(**config): class APIMethod(object): path = config['path'] payload_type = config.get('payload_type', None) payload_list = config.get('payload_list', False) allowed_param = config.get('allowed_param', []) method = config.get('method', 'GET') require_auth = config.get('require_auth', False) search_api = config.get('search_api', False) use_cache = config.get('use_cache', True) def __init__(self, api, args, kargs): # If authentication is required and no credentials # are provided, throw an error. if self.require_auth and not api.auth: raise TweepError('Authentication required!') self.api = api self.post_data = kargs.pop('post_data', None) self.retry_count = kargs.pop('retry_count', api.retry_count) self.retry_delay = kargs.pop('retry_delay', api.retry_delay) self.retry_errors = kargs.pop('retry_errors', api.retry_errors) self.headers = kargs.pop('headers', {}) self.build_parameters(args, kargs) # Pick correct URL root to use if self.search_api: self.api_root = api.search_root else: self.api_root = api.api_root # Perform any path variable substitution self.build_path() if api.secure: self.scheme = 'https://' else: self.scheme = 'http://' if self.search_api: self.host = api.search_host else: self.host = api.host # Manually set Host header to fix an issue in python 2.5 # or older where Host is set including the 443 port. # This causes Twitter to issue 301 redirect. # See Issue https://github.com/tweepy/tweepy/issues/12 self.headers['Host'] = self.host def build_parameters(self, args, kargs): self.parameters = {} for idx, arg in enumerate(args): if arg is None: continue try: self.parameters[self.allowed_param[idx]] = convert_to_utf8_str(arg) except IndexError: raise TweepError('Too many parameters supplied!') for k, arg in kargs.items(): if arg is None: continue if k in self.parameters: raise TweepError('Multiple values for parameter %s supplied!' % k) self.parameters[k] = convert_to_utf8_str(arg) def build_path(self): for variable in re_path_template.findall(self.path): name = variable.strip('{}') if name == 'user' and 'user' not in self.parameters and self.api.auth: # No 'user' parameter provided, fetch it from Auth instead. value = self.api.auth.get_username() else: try: value = urllib.quote(self.parameters[name]) except KeyError: raise TweepError('No parameter value found for path variable: %s' % name) del self.parameters[name] self.path = self.path.replace(variable, value) def execute(self): # Build the request URL url = self.api_root + self.path if len(self.parameters): url = '%s?%s' % (url, urllib.urlencode(self.parameters)) # Query the cache if one is available # and this request uses a GET method. if self.use_cache and self.api.cache and self.method == 'GET': cache_result = self.api.cache.get(url) # if cache result found and not expired, return it if cache_result: # must restore api reference if isinstance(cache_result, list): for result in cache_result: if isinstance(result, Model): result._api = self.api else: if isinstance(cache_result, Model): cache_result._api = self.api return cache_result # Continue attempting request until successful # or maximum number of retries is reached. retries_performed = 0 while retries_performed < self.retry_count + 1: # Open connection if self.api.secure: conn = httplib.HTTPSConnection(self.host, timeout=self.api.timeout) else: conn = httplib.HTTPConnection(self.host, timeout=self.api.timeout) # Apply authentication if self.api.auth: self.api.auth.apply_auth( self.scheme + self.host + url, self.method, self.headers, self.parameters ) # Request compression if configured if self.api.compression: self.headers['Accept-encoding'] = 'gzip' # Execute request try: conn.request(self.method, url, headers=self.headers, body=self.post_data) resp = conn.getresponse() except Exception, e: raise TweepError('Failed to send request: %s' % e) # Exit request loop if non-retry error code if self.retry_errors: if resp.status not in self.retry_errors: break else: if resp.status == 200: break # Sleep before retrying request again time.sleep(self.retry_delay) retries_performed += 1 # If an error was returned, throw an exception self.api.last_response = resp if resp.status != 200: try: error_msg = self.api.parser.parse_error(resp.read()) except Exception: error_msg = "Twitter error response: status code = %s" % resp.status raise TweepError(error_msg, resp) # Parse the response payload body = resp.read() if resp.getheader('Content-Encoding', '') == 'gzip': try: zipper = gzip.GzipFile(fileobj=StringIO(body)) body = zipper.read() except Exception, e: raise TweepError('Failed to decompress data: %s' % e) result = self.api.parser.parse(self, body) conn.close() # Store result into cache if one is available. if self.use_cache and self.api.cache and self.method == 'GET' and result: self.api.cache.store(url, result) return result def _call(api, *args, **kargs): method = APIMethod(api, args, kargs) return method.execute() # Set pagination mode if 'cursor' in APIMethod.allowed_param: _call.pagination_mode = 'cursor' elif 'max_id' in APIMethod.allowed_param and \ 'since_id' in APIMethod.allowed_param: _call.pagination_mode = 'id' elif 'page' in APIMethod.allowed_param: _call.pagination_mode = 'page' return _call