reverted the encoding fix that tommy made in lieu of a different crazy hack
[twitter-api-cdsw] / tweepy / binder.py
1 # Tweepy
2 # Copyright 2009-2010 Joshua Roesslein
3 # See LICENSE for details.
4
5 from __future__ import print_function
6
7 import time
8 import re
9
10 from six.moves.urllib.parse import quote
11 import requests
12
13 import logging
14
15 from tweepy.error import TweepError
16 from tweepy.utils import convert_to_utf8_str
17 from tweepy.models import Model
18
19
20 re_path_template = re.compile('{\w+}')
21
22 log = logging.getLogger('tweepy.binder')
23
24 def bind_api(**config):
25
26     class APIMethod(object):
27
28         api = config['api']
29         path = config['path']
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()
39
40         def __init__(self, args, kwargs):
41             api = self.api
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!')
46
47             self.post_data = kwargs.pop('post_data', None)
48             self.retry_count = kwargs.pop('retry_count',
49                                           api.retry_count)
50             self.retry_delay = kwargs.pop('retry_delay',
51                                           api.retry_delay)
52             self.retry_errors = kwargs.pop('retry_errors',
53                                            api.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)
61
62             # Pick correct URL root to use
63             if self.search_api:
64                 self.api_root = api.search_root
65             elif self.upload_api:
66                 self.api_root = api.upload_root
67             else:
68                 self.api_root = api.api_root
69
70             # Perform any path variable substitution
71             self.build_path()
72
73             if self.search_api:
74                 self.host = api.search_host
75             elif self.upload_api:
76                 self.host = api.upload_host
77             else:
78                 self.host = api.host
79
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
88
89         def build_parameters(self, args, kwargs):
90             self.session.params = {}
91             for idx, arg in enumerate(args):
92                 if arg is None:
93                     continue
94                 try:
95                     self.session.params[self.allowed_param[idx]] = convert_to_utf8_str(arg)
96                 except IndexError:
97                     raise TweepError('Too many parameters supplied!')
98
99             for k, arg in kwargs.items():
100                 if arg is None:
101                     continue
102                 if k in self.session.params:
103                     raise TweepError('Multiple values for parameter %s supplied!' % k)
104
105                 self.session.params[k] = convert_to_utf8_str(arg)
106
107             log.info("PARAMS: %r", self.session.params)
108
109         def build_path(self):
110             for variable in re_path_template.findall(self.path):
111                 name = variable.strip('{}')
112
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()
116                 else:
117                     try:
118                         value = quote(self.session.params[name])
119                     except KeyError:
120                         raise TweepError('No parameter value found for path variable: %s' % name)
121                     del self.session.params[name]
122
123                 self.path = self.path.replace(variable, value)
124
125         def execute(self):
126             self.api.cached_result = False
127
128             # Build the request URL
129             url = self.api_root + self.path
130             full_url = 'https://' + self.host + url
131
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
137                 if cache_result:
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
143                     else:
144                         if isinstance(cache_result, Model):
145                             cache_result._api = self.api
146                     self.api.cached_result = True
147                     return cache_result
148
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())
159                                 if sleep_time > 0:
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
163
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())
167                 #     if sleep_time > 0:
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
171
172                 # Apply authentication
173                 if self.api.auth:
174                     auth = self.api.auth.apply_auth()
175
176                 # Request compression if configured
177                 if self.api.compression:
178                     self.session.headers['Accept-encoding'] = 'gzip'
179
180                 # Execute request
181                 try:
182                     resp = self.session.request(self.method,
183                                                 full_url,
184                                                 data=self.post_data,
185                                                 timeout=self.api.timeout,
186                                                 auth=auth,
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):
201                     continue
202                 retry_delay = self.retry_delay
203                 # Exit request loop if non-retry error code
204                 if resp.status_code == 200:
205                     break
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:
210                     break
211
212                 # Sleep before retrying request again
213                 time.sleep(retry_delay)
214                 retries_performed += 1
215
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:
219                 try:
220                     error_msg = self.parser.parse_error(resp.text)
221                 except Exception:
222                     error_msg = "Twitter error response: status code = %s" % resp.status_code
223                 raise TweepError(error_msg, resp)
224
225             # Parse the response payload
226             result = self.parser.parse(self, resp.text)
227
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)
231
232             return result
233
234     def _call(*args, **kwargs):
235         method = APIMethod(args, kwargs)
236         if kwargs.get('create'):
237             return method
238         else:
239             return method.execute()
240
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'
249
250     return _call

Benjamin Mako Hill || Want to submit a patch?