Completed
Push — master ( 30829d...cf89eb )
by Messense
22:52 queued 22:20
created

BaseWeChatClient.__new__()   A

Complexity

Conditions 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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