base.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import, unicode_literals
  3. import inspect
  4. import logging
  5. import threading
  6. import requests
  7. import six
  8. from library import to_binary, to_text
  9. from library.wechatpy.client.api.base import BaseWeChatAPI
  10. from library.wechatpy.constants import WeChatErrorCode
  11. from library.wechatbase.exceptions import WeChatException, APILimitedException, WechatNetworkException
  12. from library.wechatpy.utils import json
  13. from library.wechatpy import access_token_key, jsapi_ticket_key
  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, session, timeout=None, auto_retry=True):
  28. self.appid = appid
  29. self.session = session
  30. self.timeout = timeout
  31. self.auto_retry = auto_retry
  32. self.my_retry_count = threading.local()
  33. def __str__(self):
  34. _repr = '{kclass}(appid: {appid})'.format(
  35. kclass=self.__class__.__name__,
  36. appid=self.appid)
  37. if six.PY2:
  38. return to_binary(_repr)
  39. else:
  40. return to_text(_repr)
  41. def __repr__(self):
  42. return str(self)
  43. @property
  44. def retry_count(self):
  45. if not hasattr(self.my_retry_count, 'retry_count'):
  46. self.my_retry_count.retry_count = 0
  47. return self.my_retry_count.retry_count
  48. @retry_count.setter
  49. def retry_count(self, retry_count):
  50. self.my_retry_count.retry_count = retry_count
  51. @property
  52. def access_token_key(self):
  53. return access_token_key(self.appid)
  54. @property
  55. def jsapi_ticket_key(self):
  56. return jsapi_ticket_key(self.appid)
  57. def _request(self, method, url_or_endpoint, **kwargs):
  58. if not url_or_endpoint.startswith(('http://', 'https://')):
  59. api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL)
  60. url = '{base}{endpoint}'.format(
  61. base=api_base_url,
  62. endpoint=url_or_endpoint
  63. )
  64. else:
  65. url = url_or_endpoint
  66. if 'params' not in kwargs:
  67. kwargs['params'] = {}
  68. if isinstance(kwargs['params'], dict) and \
  69. 'access_token' not in kwargs['params']:
  70. kwargs['params']['access_token'] = self.access_token
  71. if isinstance(kwargs.get('data', ''), dict):
  72. body = json.dumps(kwargs['data'], ensure_ascii=False)
  73. body = body.encode('utf-8')
  74. kwargs['data'] = body
  75. kwargs['timeout'] = kwargs.get('timeout', self.timeout)
  76. result_processor = kwargs.pop('result_processor', None)
  77. with requests.sessions.Session() as _session:
  78. res = _session.request(
  79. method=method,
  80. url=url,
  81. **kwargs
  82. )
  83. try:
  84. res.raise_for_status()
  85. except requests.RequestException as reqe:
  86. raise WechatNetworkException(
  87. errCode='HTTP{}'.format(res.status_code),
  88. errMsg=reqe.message,
  89. client=self,
  90. request=reqe.request,
  91. response=reqe.response
  92. )
  93. return self._handle_result(
  94. res, method, url, result_processor, **kwargs
  95. )
  96. def _decode_result(self, res):
  97. try:
  98. result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False)
  99. except (TypeError, ValueError):
  100. # Return origin response object if we can not decode it as JSON
  101. logger.debug('Can not decode response as JSON', exc_info=True)
  102. return res
  103. return result
  104. def _handle_result(self, res, method=None, url=None,
  105. result_processor=None, **kwargs):
  106. if not isinstance(res, dict):
  107. # Dirty hack around asyncio based AsyncWeChatClient
  108. result = self._decode_result(res)
  109. else:
  110. result = res
  111. logger.debug('Response for Wechat API<url={}> result: {}'.format(url, result))
  112. if not isinstance(result, dict):
  113. return result
  114. if 'base_resp' in result:
  115. # Different response in device APIs. Fuck tencent!
  116. result.update(result.pop('base_resp'))
  117. if 'errcode' in result:
  118. result['errcode'] = int(result['errcode'])
  119. if 'errcode' in result and result['errcode'] != 0:
  120. errcode = result['errcode']
  121. errmsg = result.get('errmsg', errcode)
  122. if errcode == WeChatErrorCode.OUT_OF_API_FREQ_LIMIT.value:
  123. # 操作频繁就直接跳出
  124. raise APILimitedException(
  125. errCode=errcode,
  126. errMsg=errmsg,
  127. client=self,
  128. request=res.request,
  129. response=res)
  130. auto_retry = False
  131. if self.auto_retry:
  132. if errcode in (WeChatErrorCode.INVALID_ACCESS_TOKEN.value, WeChatErrorCode.EXPIRED_ACCESS_TOKEN.value):
  133. auto_retry = True
  134. elif errcode in [WeChatErrorCode.SYSTEM_ERROR.value,
  135. WeChatErrorCode.SYSTEM_BUSY.value,
  136. WeChatErrorCode.INVALID_CREDENTIAL.value]:
  137. auto_retry = True
  138. if not auto_retry:
  139. raise WeChatException(
  140. errCode=errcode,
  141. errMsg=errmsg,
  142. client=self,
  143. request=res.request,
  144. response=res)
  145. self.retry_count = self.retry_count + 1
  146. if self.retry_count >= 3:
  147. logger.debug('reached the maximum number of retries. url = {}'.format(url))
  148. self.retry_count = 0
  149. raise WeChatException(
  150. errCode=errcode,
  151. errMsg=errmsg,
  152. client=self,
  153. request=res.request,
  154. response=res)
  155. else:
  156. logger.debug('retry wechat url = {}'.format(url))
  157. if errcode in (
  158. WeChatErrorCode.INVALID_ACCESS_TOKEN.value, WeChatErrorCode.EXPIRED_ACCESS_TOKEN.value):
  159. logger.info(
  160. '=== WechatToken === Access token({}) expired, fetch a new one and retry request<{}>'.format(
  161. self.appid, self.retry_count))
  162. access_token = self.fetch_access_token(kwargs['params'].get('access_token', None))
  163. kwargs['params']['access_token'] = access_token
  164. return self._request(
  165. method=method,
  166. url_or_endpoint=url,
  167. result_processor=result_processor,
  168. **kwargs)
  169. return result if not result_processor else result_processor(result)
  170. def get(self, url, **kwargs):
  171. return self._request(
  172. method='get',
  173. url_or_endpoint=url,
  174. **kwargs
  175. )
  176. def post(self, url, **kwargs):
  177. return self._request(
  178. method='post',
  179. url_or_endpoint=url,
  180. **kwargs
  181. )
  182. def fetch_jsapi_ticket(self, old_ticket=None):
  183. raise NotImplementedError()
  184. def fetch_access_token(self, old_token=None):
  185. raise NotImplementedError()
  186. @property
  187. def access_token(self):
  188. """ WeChat access token """
  189. access_token = self.session.get(self.access_token_key)
  190. if access_token:
  191. return access_token
  192. else:
  193. return self.fetch_access_token()
  194. @property
  195. def jsapi_ticket(self):
  196. jsapi_ticket = self.session.get(self.jsapi_ticket_key)
  197. if jsapi_ticket:
  198. return jsapi_ticket
  199. else:
  200. return self.fetch_jsapi_ticket()