2 # Copyright 2009-2010 Joshua Roesslein
3 # See LICENSE for details.
9 from StringIO import StringIO
12 from tweepy.error import TweepError
13 from tweepy.utils import convert_to_utf8_str
14 from tweepy.models import Model
16 re_path_template = re.compile('{\w+}')
19 def bind_api(**config):
21 class APIMethod(object):
24 payload_type = config.get('payload_type', None)
25 payload_list = config.get('payload_list', False)
26 allowed_param = config.get('allowed_param', [])
27 method = config.get('method', 'GET')
28 require_auth = config.get('require_auth', False)
29 search_api = config.get('search_api', False)
30 use_cache = config.get('use_cache', True)
32 def __init__(self, api, args, kargs):
33 # If authentication is required and no credentials
34 # are provided, throw an error.
35 if self.require_auth and not api.auth:
36 raise TweepError('Authentication required!')
39 self.post_data = kargs.pop('post_data', None)
40 self.retry_count = kargs.pop('retry_count', api.retry_count)
41 self.retry_delay = kargs.pop('retry_delay', api.retry_delay)
42 self.retry_errors = kargs.pop('retry_errors', api.retry_errors)
43 self.headers = kargs.pop('headers', {})
44 self.build_parameters(args, kargs)
46 # Pick correct URL root to use
48 self.api_root = api.search_root
50 self.api_root = api.api_root
52 # Perform any path variable substitution
56 self.scheme = 'https://'
58 self.scheme = 'http://'
61 self.host = api.search_host
65 # Manually set Host header to fix an issue in python 2.5
66 # or older where Host is set including the 443 port.
67 # This causes Twitter to issue 301 redirect.
68 # See Issue https://github.com/tweepy/tweepy/issues/12
69 self.headers['Host'] = self.host
71 def build_parameters(self, args, kargs):
73 for idx, arg in enumerate(args):
78 self.parameters[self.allowed_param[idx]] = convert_to_utf8_str(arg)
80 raise TweepError('Too many parameters supplied!')
82 for k, arg in kargs.items():
85 if k in self.parameters:
86 raise TweepError('Multiple values for parameter %s supplied!' % k)
88 self.parameters[k] = convert_to_utf8_str(arg)
91 for variable in re_path_template.findall(self.path):
92 name = variable.strip('{}')
94 if name == 'user' and 'user' not in self.parameters and self.api.auth:
95 # No 'user' parameter provided, fetch it from Auth instead.
96 value = self.api.auth.get_username()
99 value = urllib.quote(self.parameters[name])
101 raise TweepError('No parameter value found for path variable: %s' % name)
102 del self.parameters[name]
104 self.path = self.path.replace(variable, value)
107 self.api.cached_result = False
109 # Build the request URL
110 url = self.api_root + self.path
111 if len(self.parameters):
112 url = '%s?%s' % (url, urllib.urlencode(self.parameters))
114 # Query the cache if one is available
115 # and this request uses a GET method.
116 if self.use_cache and self.api.cache and self.method == 'GET':
117 cache_result = self.api.cache.get(url)
118 # if cache result found and not expired, return it
120 # must restore api reference
121 if isinstance(cache_result, list):
122 for result in cache_result:
123 if isinstance(result, Model):
124 result._api = self.api
126 if isinstance(cache_result, Model):
127 cache_result._api = self.api
128 self.api.cached_result = True
131 # Continue attempting request until successful
132 # or maximum number of retries is reached.
133 retries_performed = 0
134 while retries_performed < self.retry_count + 1:
137 conn = httplib.HTTPSConnection(self.host, timeout=self.api.timeout)
139 conn = httplib.HTTPConnection(self.host, timeout=self.api.timeout)
141 # Apply authentication
143 self.api.auth.apply_auth(
144 self.scheme + self.host + url,
145 self.method, self.headers, self.parameters
148 # Request compression if configured
149 if self.api.compression:
150 self.headers['Accept-encoding'] = 'gzip'
154 conn.request(self.method, url, headers=self.headers, body=self.post_data)
155 resp = conn.getresponse()
156 except Exception as e:
157 raise TweepError('Failed to send request: %s' % e)
159 # Exit request loop if non-retry error code
160 if self.retry_errors:
161 if resp.status not in self.retry_errors: break
163 if resp.status == 200: break
165 # Sleep before retrying request again
166 time.sleep(self.retry_delay)
167 retries_performed += 1
169 # If an error was returned, throw an exception
170 self.api.last_response = resp
171 if resp.status and not 200 <= resp.status < 300:
173 error_msg = self.api.parser.parse_error(resp.read())
175 error_msg = "Twitter error response: status code = %s" % resp.status
176 raise TweepError(error_msg, resp)
178 # Parse the response payload
180 if resp.getheader('Content-Encoding', '') == 'gzip':
182 zipper = gzip.GzipFile(fileobj=StringIO(body))
184 except Exception as e:
185 raise TweepError('Failed to decompress data: %s' % e)
186 result = self.api.parser.parse(self, body)
190 # Store result into cache if one is available.
191 if self.use_cache and self.api.cache and self.method == 'GET' and result:
192 self.api.cache.store(url, result)
197 def _call(api, *args, **kargs):
199 method = APIMethod(api, args, kargs)
200 return method.execute()
203 # Set pagination mode
204 if 'cursor' in APIMethod.allowed_param:
205 _call.pagination_mode = 'cursor'
206 elif 'max_id' in APIMethod.allowed_param and \
207 'since_id' in APIMethod.allowed_param:
208 _call.pagination_mode = 'id'
209 elif 'page' in APIMethod.allowed_param:
210 _call.pagination_mode = 'page'