Completed
Pull Request — master (#241)
by Messense
30:42 queued 10:42
created

BaseWeChatClient.__new__()   A

Complexity

Conditions 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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