binder.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. # Tweepy
  2. # Copyright 2009-2010 Joshua Roesslein
  3. # See LICENSE for details.
  4. from __future__ import print_function
  5. import time
  6. import re
  7. from six.moves.urllib.parse import quote
  8. import requests
  9. import logging
  10. from tweepy.error import TweepError, RateLimitError, is_rate_limit_error_message
  11. from tweepy.utils import convert_to_utf8_str
  12. from tweepy.models import Model
  13. re_path_template = re.compile('{\w+}')
  14. log = logging.getLogger('tweepy.binder')
  15. def bind_api(**config):
  16. class APIMethod(object):
  17. api = config['api']
  18. path = config['path']
  19. payload_type = config.get('payload_type', None)
  20. payload_list = config.get('payload_list', False)
  21. allowed_param = config.get('allowed_param', [])
  22. method = config.get('method', 'GET')
  23. require_auth = config.get('require_auth', False)
  24. search_api = config.get('search_api', False)
  25. upload_api = config.get('upload_api', False)
  26. use_cache = config.get('use_cache', True)
  27. session = requests.Session()
  28. def __init__(self, args, kwargs):
  29. api = self.api
  30. # If authentication is required and no credentials
  31. # are provided, throw an error.
  32. if self.require_auth and not api.auth:
  33. raise TweepError('Authentication required!')
  34. self.post_data = kwargs.pop('post_data', None)
  35. self.retry_count = kwargs.pop('retry_count',
  36. api.retry_count)
  37. self.retry_delay = kwargs.pop('retry_delay',
  38. api.retry_delay)
  39. self.retry_errors = kwargs.pop('retry_errors',
  40. api.retry_errors)
  41. self.wait_on_rate_limit = kwargs.pop('wait_on_rate_limit',
  42. api.wait_on_rate_limit)
  43. self.wait_on_rate_limit_notify = kwargs.pop('wait_on_rate_limit_notify',
  44. api.wait_on_rate_limit_notify)
  45. self.parser = kwargs.pop('parser', api.parser)
  46. self.session.headers = kwargs.pop('headers', {})
  47. self.build_parameters(args, kwargs)
  48. # Pick correct URL root to use
  49. if self.search_api:
  50. self.api_root = api.search_root
  51. elif self.upload_api:
  52. self.api_root = api.upload_root
  53. else:
  54. self.api_root = api.api_root
  55. # Perform any path variable substitution
  56. self.build_path()
  57. if self.search_api:
  58. self.host = api.search_host
  59. elif self.upload_api:
  60. self.host = api.upload_host
  61. else:
  62. self.host = api.host
  63. # Manually set Host header to fix an issue in python 2.5
  64. # or older where Host is set including the 443 port.
  65. # This causes Twitter to issue 301 redirect.
  66. # See Issue https://github.com/tweepy/tweepy/issues/12
  67. self.session.headers['Host'] = self.host
  68. # Monitoring rate limits
  69. self._remaining_calls = None
  70. self._reset_time = None
  71. def build_parameters(self, args, kwargs):
  72. self.session.params = {}
  73. for idx, arg in enumerate(args):
  74. if arg is None:
  75. continue
  76. try:
  77. self.session.params[self.allowed_param[idx]] = convert_to_utf8_str(arg)
  78. except IndexError:
  79. raise TweepError('Too many parameters supplied!')
  80. for k, arg in kwargs.items():
  81. if arg is None:
  82. continue
  83. if k in self.session.params:
  84. raise TweepError('Multiple values for parameter %s supplied!' % k)
  85. self.session.params[k] = convert_to_utf8_str(arg)
  86. log.info("PARAMS: %r", self.session.params)
  87. def build_path(self):
  88. for variable in re_path_template.findall(self.path):
  89. name = variable.strip('{}')
  90. if name == 'user' and 'user' not in self.session.params and self.api.auth:
  91. # No 'user' parameter provided, fetch it from Auth instead.
  92. value = self.api.auth.get_username()
  93. else:
  94. try:
  95. value = quote(self.session.params[name])
  96. except KeyError:
  97. raise TweepError('No parameter value found for path variable: %s' % name)
  98. del self.session.params[name]
  99. self.path = self.path.replace(variable, value)
  100. def execute(self):
  101. self.api.cached_result = False
  102. # Build the request URL
  103. url = self.api_root + self.path
  104. full_url = 'https://' + self.host + url
  105. # Query the cache if one is available
  106. # and this request uses a GET method.
  107. if self.use_cache and self.api.cache and self.method == 'GET':
  108. cache_result = self.api.cache.get(url)
  109. # if cache result found and not expired, return it
  110. if cache_result:
  111. # must restore api reference
  112. if isinstance(cache_result, list):
  113. for result in cache_result:
  114. if isinstance(result, Model):
  115. result._api = self.api
  116. else:
  117. if isinstance(cache_result, Model):
  118. cache_result._api = self.api
  119. self.api.cached_result = True
  120. return cache_result
  121. # Continue attempting request until successful
  122. # or maximum number of retries is reached.
  123. retries_performed = 0
  124. while retries_performed < self.retry_count + 1:
  125. # handle running out of api calls
  126. if self.wait_on_rate_limit:
  127. if self._reset_time is not None:
  128. if self._remaining_calls is not None:
  129. if self._remaining_calls < 1:
  130. sleep_time = self._reset_time - int(time.time())
  131. if sleep_time > 0:
  132. if self.wait_on_rate_limit_notify:
  133. print("Rate limit reached. Sleeping for:", sleep_time)
  134. time.sleep(sleep_time + 5) # sleep for few extra sec
  135. # if self.wait_on_rate_limit and self._reset_time is not None and \
  136. # self._remaining_calls is not None and self._remaining_calls < 1:
  137. # sleep_time = self._reset_time - int(time.time())
  138. # if sleep_time > 0:
  139. # if self.wait_on_rate_limit_notify:
  140. # print("Rate limit reached. Sleeping for: " + str(sleep_time))
  141. # time.sleep(sleep_time + 5) # sleep for few extra sec
  142. # Apply authentication
  143. if self.api.auth:
  144. auth = self.api.auth.apply_auth()
  145. # Request compression if configured
  146. if self.api.compression:
  147. self.session.headers['Accept-encoding'] = 'gzip'
  148. # Execute request
  149. try:
  150. resp = self.session.request(self.method,
  151. full_url,
  152. data=self.post_data,
  153. timeout=self.api.timeout,
  154. auth=auth,
  155. proxies=self.api.proxy)
  156. except Exception as e:
  157. raise TweepError('Failed to send request: %s' % e)
  158. rem_calls = resp.headers.get('x-rate-limit-remaining')
  159. if rem_calls is not None:
  160. self._remaining_calls = int(rem_calls)
  161. elif isinstance(self._remaining_calls, int):
  162. self._remaining_calls -= 1
  163. reset_time = resp.headers.get('x-rate-limit-reset')
  164. if reset_time is not None:
  165. self._reset_time = int(reset_time)
  166. if self.wait_on_rate_limit and self._remaining_calls == 0 and (
  167. # if ran out of calls before waiting switching retry last call
  168. resp.status_code == 429 or resp.status_code == 420):
  169. continue
  170. retry_delay = self.retry_delay
  171. # Exit request loop if non-retry error code
  172. if resp.status_code == 200:
  173. break
  174. elif (resp.status_code == 429 or resp.status_code == 420) and self.wait_on_rate_limit:
  175. if 'retry-after' in resp.headers:
  176. retry_delay = float(resp.headers['retry-after'])
  177. elif self.retry_errors and resp.status_code not in self.retry_errors:
  178. break
  179. # Sleep before retrying request again
  180. time.sleep(retry_delay)
  181. retries_performed += 1
  182. # If an error was returned, throw an exception
  183. self.api.last_response = resp
  184. if resp.status_code and not 200 <= resp.status_code < 300:
  185. try:
  186. error_msg, api_error_code = \
  187. self.parser.parse_error(resp.text)
  188. except Exception:
  189. error_msg = "Twitter error response: status code = %s" % resp.status_code
  190. api_error_code = None
  191. if is_rate_limit_error_message(error_msg):
  192. raise RateLimitError(error_msg, resp)
  193. else:
  194. raise TweepError(error_msg, resp, api_code=api_error_code)
  195. # Parse the response payload
  196. result = self.parser.parse(self, resp.text)
  197. # Store result into cache if one is available.
  198. if self.use_cache and self.api.cache and self.method == 'GET' and result:
  199. self.api.cache.store(url, result)
  200. return result
  201. def _call(*args, **kwargs):
  202. method = APIMethod(args, kwargs)
  203. if kwargs.get('create'):
  204. return method
  205. else:
  206. return method.execute()
  207. # Set pagination mode
  208. if 'cursor' in APIMethod.allowed_param:
  209. _call.pagination_mode = 'cursor'
  210. elif 'max_id' in APIMethod.allowed_param:
  211. if 'since_id' in APIMethod.allowed_param:
  212. _call.pagination_mode = 'id'
  213. elif 'page' in APIMethod.allowed_param:
  214. _call.pagination_mode = 'page'
  215. return _call