2 # Copyright 2009-2010 Joshua Roesslein
3 # See LICENSE for details.
5 from __future__ import print_function
10 from six.moves.urllib.parse import quote
15 from tweepy.error import TweepError
16 from tweepy.utils import convert_to_utf8_str
17 from tweepy.models import Model
20 re_path_template = re.compile('{\w+}')
22 log = logging.getLogger('tweepy.binder')
24 def bind_api(**config):
26 class APIMethod(object):
30 payload_type = config.get('payload_type', None)
31 payload_list = config.get('payload_list', False)
32 allowed_param = config.get('allowed_param', [])
33 method = config.get('method', 'GET')
34 require_auth = config.get('require_auth', False)
35 search_api = config.get('search_api', False)
36 upload_api = config.get('upload_api', False)
37 use_cache = config.get('use_cache', True)
38 session = requests.Session()
40 def __init__(self, args, kwargs):
42 # If authentication is required and no credentials
43 # are provided, throw an error.
44 if self.require_auth and not api.auth:
45 raise TweepError('Authentication required!')
47 self.post_data = kwargs.pop('post_data', None)
48 self.retry_count = kwargs.pop('retry_count',
50 self.retry_delay = kwargs.pop('retry_delay',
52 self.retry_errors = kwargs.pop('retry_errors',
54 self.wait_on_rate_limit = kwargs.pop('wait_on_rate_limit',
55 api.wait_on_rate_limit)
56 self.wait_on_rate_limit_notify = kwargs.pop('wait_on_rate_limit_notify',
57 api.wait_on_rate_limit_notify)
58 self.parser = kwargs.pop('parser', api.parser)
59 self.session.headers = kwargs.pop('headers', {})
60 self.build_parameters(args, kwargs)
62 # Pick correct URL root to use
64 self.api_root = api.search_root
66 self.api_root = api.upload_root
68 self.api_root = api.api_root
70 # Perform any path variable substitution
74 self.host = api.search_host
76 self.host = api.upload_host
80 # Manually set Host header to fix an issue in python 2.5
81 # or older where Host is set including the 443 port.
82 # This causes Twitter to issue 301 redirect.
83 # See Issue https://github.com/tweepy/tweepy/issues/12
84 self.session.headers['Host'] = self.host
85 # Monitoring rate limits
86 self._remaining_calls = None
87 self._reset_time = None
89 def build_parameters(self, args, kwargs):
90 self.session.params = {}
91 for idx, arg in enumerate(args):
95 self.session.params[self.allowed_param[idx]] = convert_to_utf8_str(arg)
97 raise TweepError('Too many parameters supplied!')
99 for k, arg in kwargs.items():
102 if k in self.session.params:
103 raise TweepError('Multiple values for parameter %s supplied!' % k)
105 self.session.params[k] = convert_to_utf8_str(arg)
107 log.info("PARAMS: %r", self.session.params)
109 def build_path(self):
110 for variable in re_path_template.findall(self.path):
111 name = variable.strip('{}')
113 if name == 'user' and 'user' not in self.session.params and self.api.auth:
114 # No 'user' parameter provided, fetch it from Auth instead.
115 value = self.api.auth.get_username()
118 value = quote(self.session.params[name])
120 raise TweepError('No parameter value found for path variable: %s' % name)
121 del self.session.params[name]
123 self.path = self.path.replace(variable, value)
126 self.api.cached_result = False
128 # Build the request URL
129 url = self.api_root + self.path
130 full_url = 'https://' + self.host + url
132 # Query the cache if one is available
133 # and this request uses a GET method.
134 if self.use_cache and self.api.cache and self.method == 'GET':
135 cache_result = self.api.cache.get(url)
136 # if cache result found and not expired, return it
138 # must restore api reference
139 if isinstance(cache_result, list):
140 for result in cache_result:
141 if isinstance(result, Model):
142 result._api = self.api
144 if isinstance(cache_result, Model):
145 cache_result._api = self.api
146 self.api.cached_result = True
149 # Continue attempting request until successful
150 # or maximum number of retries is reached.
151 retries_performed = 0
152 while retries_performed < self.retry_count + 1:
153 # handle running out of api calls
154 if self.wait_on_rate_limit:
155 if self._reset_time is not None:
156 if self._remaining_calls is not None:
157 if self._remaining_calls < 1:
158 sleep_time = self._reset_time - int(time.time())
160 if self.wait_on_rate_limit_notify:
161 print("Rate limit reached. Sleeping for:", sleep_time)
162 time.sleep(sleep_time + 5) # sleep for few extra sec
164 # if self.wait_on_rate_limit and self._reset_time is not None and \
165 # self._remaining_calls is not None and self._remaining_calls < 1:
166 # sleep_time = self._reset_time - int(time.time())
168 # if self.wait_on_rate_limit_notify:
169 # print("Rate limit reached. Sleeping for: " + str(sleep_time))
170 # time.sleep(sleep_time + 5) # sleep for few extra sec
172 # Apply authentication
174 auth = self.api.auth.apply_auth()
176 # Request compression if configured
177 if self.api.compression:
178 self.session.headers['Accept-encoding'] = 'gzip'
182 resp = self.session.request(self.method,
185 timeout=self.api.timeout,
187 proxies=self.api.proxy)
188 except Exception as e:
189 raise TweepError('Failed to send request: %s' % e)
190 rem_calls = resp.headers.get('x-rate-limit-remaining')
191 if rem_calls is not None:
192 self._remaining_calls = int(rem_calls)
193 elif isinstance(self._remaining_calls, int):
194 self._remaining_calls -= 1
195 reset_time = resp.headers.get('x-rate-limit-reset')
196 if reset_time is not None:
197 self._reset_time = int(reset_time)
198 if self.wait_on_rate_limit and self._remaining_calls == 0 and (
199 # if ran out of calls before waiting switching retry last call
200 resp.status_code == 429 or resp.status_code == 420):
202 retry_delay = self.retry_delay
203 # Exit request loop if non-retry error code
204 if resp.status_code == 200:
206 elif (resp.status_code == 429 or resp.status_code == 420) and self.wait_on_rate_limit:
207 if 'retry-after' in resp.headers:
208 retry_delay = float(resp.headers['retry-after'])
209 elif self.retry_errors and resp.status_code not in self.retry_errors:
212 # Sleep before retrying request again
213 time.sleep(retry_delay)
214 retries_performed += 1
216 # If an error was returned, throw an exception
217 self.api.last_response = resp
218 if resp.status_code and not 200 <= resp.status_code < 300:
220 error_msg = self.parser.parse_error(resp.text)
222 error_msg = "Twitter error response: status code = %s" % resp.status_code
223 raise TweepError(error_msg, resp)
225 # Parse the response payload
226 result = self.parser.parse(self, resp.text)
228 # Store result into cache if one is available.
229 if self.use_cache and self.api.cache and self.method == 'GET' and result:
230 self.api.cache.store(url, result)
234 def _call(*args, **kwargs):
235 method = APIMethod(args, kwargs)
236 if kwargs.get('create'):
239 return method.execute()
241 # Set pagination mode
242 if 'cursor' in APIMethod.allowed_param:
243 _call.pagination_mode = 'cursor'
244 elif 'max_id' in APIMethod.allowed_param:
245 if 'since_id' in APIMethod.allowed_param:
246 _call.pagination_mode = 'id'
247 elif 'page' in APIMethod.allowed_param:
248 _call.pagination_mode = 'page'