base.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import, unicode_literals
  3. import time
  4. import inspect
  5. import logging
  6. import warnings
  7. import six
  8. import requests
  9. from wechatpy.constants import WeChatErrorCode
  10. from wechatpy.utils import json, get_querystring
  11. from wechatpy.session.memorystorage import MemoryStorage
  12. from wechatpy.exceptions import WeChatClientException, APILimitedException
  13. from wechatpy.client.api.base import BaseWeChatAPI
  14. logger = logging.getLogger(__name__)
  15. def _is_api_endpoint(obj):
  16. return isinstance(obj, BaseWeChatAPI)
  17. class BaseWeChatClient(object):
  18. API_BASE_URL = ''
  19. def __new__(cls, *args, **kwargs):
  20. self = super(BaseWeChatClient, cls).__new__(cls)
  21. api_endpoints = inspect.getmembers(self, _is_api_endpoint)
  22. for name, api in api_endpoints:
  23. api_cls = type(api)
  24. api = api_cls(self)
  25. setattr(self, name, api)
  26. return self
  27. def __init__(self, appid, access_token=None, session=None, timeout=None, auto_retry=True):
  28. self._http = requests.Session()
  29. self.appid = appid
  30. self.expires_at = None
  31. self.session = session or MemoryStorage()
  32. self.timeout = timeout
  33. self.auto_retry = auto_retry
  34. if isinstance(session, six.string_types):
  35. from shove import Shove
  36. from wechatpy.session.shovestorage import ShoveStorage
  37. querystring = get_querystring(session)
  38. prefix = querystring.get('prefix', ['wechatpy'])[0]
  39. shove = Shove(session)
  40. storage = ShoveStorage(shove, prefix)
  41. self.session = storage
  42. if access_token:
  43. self.session.set(self.access_token_key, access_token)
  44. @property
  45. def access_token_key(self):
  46. return '{0}_access_token'.format(self.appid)
  47. def _request(self, method, url_or_endpoint, **kwargs):
  48. if not url_or_endpoint.startswith(('http://', 'https://')):
  49. api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL)
  50. url = '{base}{endpoint}'.format(
  51. base=api_base_url,
  52. endpoint=url_or_endpoint
  53. )
  54. else:
  55. url = url_or_endpoint
  56. if 'params' not in kwargs:
  57. kwargs['params'] = {}
  58. if isinstance(kwargs['params'], dict) and \
  59. 'access_token' not in kwargs['params']:
  60. kwargs['params']['access_token'] = self.access_token
  61. if isinstance(kwargs.get('data', ''), dict):
  62. body = json.dumps(kwargs['data'], ensure_ascii=False)
  63. body = body.encode('utf-8')
  64. kwargs['data'] = body
  65. kwargs['timeout'] = kwargs.get('timeout', self.timeout)
  66. result_processor = kwargs.pop('result_processor', None)
  67. res = self._http.request(
  68. method=method,
  69. url=url,
  70. **kwargs
  71. )
  72. try:
  73. res.raise_for_status()
  74. except requests.RequestException as reqe:
  75. raise WeChatClientException(
  76. errcode=None,
  77. errmsg=None,
  78. client=self,
  79. request=reqe.request,
  80. response=reqe.response
  81. )
  82. return self._handle_result(
  83. res, method, url, result_processor, **kwargs
  84. )
  85. def _decode_result(self, res):
  86. try:
  87. result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False)
  88. except (TypeError, ValueError):
  89. # Return origin response object if we can not decode it as JSON
  90. logger.debug('Can not decode response as JSON', exc_info=True)
  91. return res
  92. return result
  93. def _handle_result(self, res, method=None, url=None,
  94. result_processor=None, **kwargs):
  95. if not isinstance(res, dict):
  96. # Dirty hack around asyncio based AsyncWeChatClient
  97. result = self._decode_result(res)
  98. else:
  99. result = res
  100. if not isinstance(result, dict):
  101. return result
  102. if 'base_resp' in result:
  103. # Different response in device APIs. Fuck tencent!
  104. result.update(result.pop('base_resp'))
  105. if 'errcode' in result:
  106. result['errcode'] = int(result['errcode'])
  107. if 'errcode' in result and result['errcode'] != 0:
  108. errcode = result['errcode']
  109. errmsg = result.get('errmsg', errcode)
  110. if self.auto_retry and errcode in (
  111. WeChatErrorCode.INVALID_CREDENTIAL.value,
  112. WeChatErrorCode.INVALID_ACCESS_TOKEN.value,
  113. WeChatErrorCode.EXPIRED_ACCESS_TOKEN.value):
  114. logger.info('Access token expired, fetch a new one and retry request')
  115. self.fetch_access_token()
  116. access_token = self.session.get(self.access_token_key)
  117. kwargs['params']['access_token'] = access_token
  118. return self._request(
  119. method=method,
  120. url_or_endpoint=url,
  121. result_processor=result_processor,
  122. **kwargs
  123. )
  124. elif errcode == WeChatErrorCode.OUT_OF_API_FREQ_LIMIT.value:
  125. # api freq out of limit
  126. raise APILimitedException(
  127. errcode,
  128. errmsg,
  129. client=self,
  130. request=res.request,
  131. response=res
  132. )
  133. else:
  134. raise WeChatClientException(
  135. errcode,
  136. errmsg,
  137. client=self,
  138. request=res.request,
  139. response=res
  140. )
  141. return result if not result_processor else result_processor(result)
  142. def get(self, url, **kwargs):
  143. return self._request(
  144. method='get',
  145. url_or_endpoint=url,
  146. **kwargs
  147. )
  148. def _get(self, url, **kwargs):
  149. warnings.warn('`_get` method of `WeChatClient` is deprecated, will be removed in 1.6,'
  150. 'Use `get` instead',
  151. DeprecationWarning, stacklevel=2)
  152. return self.get(url, **kwargs)
  153. def post(self, url, **kwargs):
  154. return self._request(
  155. method='post',
  156. url_or_endpoint=url,
  157. **kwargs
  158. )
  159. def _post(self, url, **kwargs):
  160. warnings.warn('`_post` method of `WeChatClient` is deprecated, will be removed in 1.6,'
  161. 'Use `post` instead',
  162. DeprecationWarning, stacklevel=2)
  163. return self.post(url, **kwargs)
  164. def _fetch_access_token(self, url, params):
  165. """ The real fetch access token """
  166. logger.info('Fetching access token')
  167. res = self._http.get(
  168. url=url,
  169. params=params
  170. )
  171. try:
  172. res.raise_for_status()
  173. except requests.RequestException as reqe:
  174. raise WeChatClientException(
  175. errcode=None,
  176. errmsg=None,
  177. client=self,
  178. request=reqe.request,
  179. response=reqe.response
  180. )
  181. result = res.json()
  182. if 'errcode' in result and result['errcode'] != 0:
  183. raise WeChatClientException(
  184. result['errcode'],
  185. result['errmsg'],
  186. client=self,
  187. request=res.request,
  188. response=res
  189. )
  190. expires_in = 7200
  191. if 'expires_in' in result:
  192. expires_in = result['expires_in']
  193. self.session.set(
  194. self.access_token_key,
  195. result['access_token'],
  196. expires_in
  197. )
  198. self.expires_at = int(time.time()) + expires_in
  199. return result
  200. def fetch_access_token(self):
  201. raise NotImplementedError()
  202. @property
  203. def access_token(self):
  204. """ WeChat access token """
  205. access_token = self.session.get(self.access_token_key)
  206. if access_token:
  207. if not self.expires_at:
  208. # user provided access_token, just return it
  209. return access_token
  210. timestamp = time.time()
  211. if self.expires_at - timestamp > 60:
  212. return access_token
  213. self.fetch_access_token()
  214. return self.session.get(self.access_token_key)