# -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals import inspect import logging import threading import requests import six from library import to_binary, to_text from library.wechatpy.client.api.base import BaseWeChatAPI from library.wechatpy.constants import WeChatErrorCode from library.wechatbase.exceptions import WeChatException, APILimitedException, WechatNetworkException from library.wechatpy.utils import json from library.wechatpy import access_token_key, jsapi_ticket_key logger = logging.getLogger(__name__) def _is_api_endpoint(obj): return isinstance(obj, BaseWeChatAPI) class BaseWeChatClient(object): API_BASE_URL = '' def __new__(cls, *args, **kwargs): self = super(BaseWeChatClient, cls).__new__(cls) api_endpoints = inspect.getmembers(self, _is_api_endpoint) for name, api in api_endpoints: api_cls = type(api) api = api_cls(self) setattr(self, name, api) return self def __init__(self, appid, session, timeout=None, auto_retry=True): self.appid = appid self.session = session self.timeout = timeout self.auto_retry = auto_retry self.my_retry_count = threading.local() def __str__(self): _repr = '{kclass}(appid: {appid})'.format( kclass=self.__class__.__name__, appid=self.appid) if six.PY2: return to_binary(_repr) else: return to_text(_repr) def __repr__(self): return str(self) @property def retry_count(self): if not hasattr(self.my_retry_count, 'retry_count'): self.my_retry_count.retry_count = 0 return self.my_retry_count.retry_count @retry_count.setter def retry_count(self, retry_count): self.my_retry_count.retry_count = retry_count @property def access_token_key(self): return access_token_key(self.appid) @property def jsapi_ticket_key(self): return jsapi_ticket_key(self.appid) def _request(self, method, url_or_endpoint, **kwargs): if not url_or_endpoint.startswith(('http://', 'https://')): api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL) url = '{base}{endpoint}'.format( base=api_base_url, endpoint=url_or_endpoint ) else: url = url_or_endpoint if 'params' not in kwargs: kwargs['params'] = {} if isinstance(kwargs['params'], dict) and \ 'access_token' not in kwargs['params']: kwargs['params']['access_token'] = self.access_token if isinstance(kwargs.get('data', ''), dict): body = json.dumps(kwargs['data'], ensure_ascii=False) body = body.encode('utf-8') kwargs['data'] = body kwargs['timeout'] = kwargs.get('timeout', self.timeout) result_processor = kwargs.pop('result_processor', None) with requests.sessions.Session() as _session: res = _session.request( method=method, url=url, **kwargs ) try: res.raise_for_status() except requests.RequestException as reqe: raise WechatNetworkException( errCode='HTTP{}'.format(res.status_code), errMsg=reqe.message, client=self, request=reqe.request, response=reqe.response ) return self._handle_result( res, method, url, result_processor, **kwargs ) def _decode_result(self, res): try: result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False) except (TypeError, ValueError): # Return origin response object if we can not decode it as JSON logger.debug('Can not decode response as JSON', exc_info=True) return res return result def _handle_result(self, res, method=None, url=None, result_processor=None, **kwargs): if not isinstance(res, dict): # Dirty hack around asyncio based AsyncWeChatClient result = self._decode_result(res) else: result = res logger.debug('Response for Wechat API result: {}'.format(url, result)) if not isinstance(result, dict): return result if 'base_resp' in result: # Different response in device APIs. Fuck tencent! result.update(result.pop('base_resp')) if 'errcode' in result: result['errcode'] = int(result['errcode']) if 'errcode' in result and result['errcode'] != 0: errcode = result['errcode'] errmsg = result.get('errmsg', errcode) if errcode == WeChatErrorCode.OUT_OF_API_FREQ_LIMIT.value: # 操作频繁就直接跳出 raise APILimitedException( errCode=errcode, errMsg=errmsg, client=self, request=res.request, response=res) auto_retry = False if self.auto_retry: if errcode in (WeChatErrorCode.INVALID_ACCESS_TOKEN.value, WeChatErrorCode.EXPIRED_ACCESS_TOKEN.value): auto_retry = True elif errcode in [WeChatErrorCode.SYSTEM_ERROR.value, WeChatErrorCode.SYSTEM_BUSY.value, WeChatErrorCode.INVALID_CREDENTIAL.value]: auto_retry = True if not auto_retry: raise WeChatException( errCode=errcode, errMsg=errmsg, client=self, request=res.request, response=res) self.retry_count = self.retry_count + 1 if self.retry_count >= 3: logger.debug('reached the maximum number of retries. url = {}'.format(url)) self.retry_count = 0 raise WeChatException( errCode=errcode, errMsg=errmsg, client=self, request=res.request, response=res) else: logger.debug('retry wechat url = {}'.format(url)) if errcode in ( WeChatErrorCode.INVALID_ACCESS_TOKEN.value, WeChatErrorCode.EXPIRED_ACCESS_TOKEN.value): logger.info( '=== WechatToken === Access token({}) expired, fetch a new one and retry request<{}>'.format( self.appid, self.retry_count)) access_token = self.fetch_access_token(kwargs['params'].get('access_token', None)) kwargs['params']['access_token'] = access_token return self._request( method=method, url_or_endpoint=url, result_processor=result_processor, **kwargs) return result if not result_processor else result_processor(result) def get(self, url, **kwargs): return self._request( method='get', url_or_endpoint=url, **kwargs ) def post(self, url, **kwargs): return self._request( method='post', url_or_endpoint=url, **kwargs ) def fetch_jsapi_ticket(self, old_ticket=None): raise NotImplementedError() def fetch_access_token(self, old_token=None): raise NotImplementedError() @property def access_token(self): """ WeChat access token """ access_token = self.session.get(self.access_token_key) if access_token: return access_token else: return self.fetch_access_token() @property def jsapi_ticket(self): jsapi_ticket = self.session.get(self.jsapi_ticket_key) if jsapi_ticket: return jsapi_ticket else: return self.fetch_jsapi_ticket()