├── .gitignore
├── README
├── README.md
├── setup.py
├── test
└── test.py
└── weixin.py
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | *.pyc
3 | wxtest.py
4 | newtest.py
5 | access_token
6 | Makefile
7 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | Python client SDK for Micro Message Public Platform API.
2 |
3 | You can download the source code at:
4 |
5 | https://github.com/kun945/weixinpy
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # weixinpy
2 | ----
3 | 这是一个简单的python版本的腾讯微信公共平台的sdk。
4 |
5 | ## 更新日志
6 | ----
7 |
8 | ### 当前版本 version 0.5.0
9 | ----
10 | 1、HTTP GET方法关键字由'_get'改为'dget'(原来的'_get'仍然可用)。
11 | 2、修改完善媒体文件的上传(支持"mpeg4/jpeg/jpg/png/gif/bmp/mp3/wma/wav/amr"), 具体看usage。
12 |
13 | ### version 0.4.9
14 | ----
15 | 1、修复bug。
16 | 2、默认采用FileCache方式保存access_token。
17 | 3、增加对“微信智能接口”、“微信摇一摇周边”、“网页授权”、“数据统计”、“微信小店”的支持。
18 |
19 | ## Usage
20 | ----
21 | 初始化过程:
22 | ```
23 | from weixin import WeixinClient, APIError, AccessTokenError
24 | # 如果你有使用python-memcache 可以使用fc=False,path='ip:port'来启用memcache
25 | # 默认使用FileCache, 保存在/tmp/access_token, 可以通过path='path'来设置保存目录
26 | wc = WeiXinClient('您的AppID', '您的AppSecret')
27 | # 使用其他api前必须先获取token
28 | wc.request_access_token()
29 | ```
30 |
31 | 请求用户列表:
32 | API URL:https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID
33 | ```
34 | # 我们这里是第一次调用所以next_openid=None
35 | rjson = wc.user.get.dget(next_openid=None)
36 | count = rjson.count
37 | id_list = rjson.data.openid
38 | while count < rjson.total:
39 | rjson = wc.user.get.dget(next_openid=rjson.next_openid)
40 | count += rjson.count
41 | id_list.extend(rjson.openid)
42 | # 最后看看都有哪些用户
43 | print id_list
44 | ```
45 |
46 | 发送文字信息:
47 | API URL:https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
48 | ```
49 | for uid in id_list:
50 | content = '{"touser":"%s", "msgtype":"text", "text":{ "content":"hello!"}}' %uid
51 | #print 可以看有没有发送成功, 可以捕获api错误异常
52 | try:
53 | print wc.message.custom.send.post(body=content)
54 | except APIError, e:
55 | print e, uid
56 | ```
57 |
58 | 上传并发送媒体:
59 | API URL:http://file.api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
60 | ```
61 | rjson = wc.media.upload.file(type='image', png=open('/home/pi/ocr_pi.png', 'rb'))
62 | print rjson
63 | # 把上传的图片发出去
64 | for uid in id_list:
65 | content = '{"touser":"%s", "msgtype":"image", ' \
66 | '"image":{ "media_id":"%s"}}' % (uid, rjson.media_id)
67 | print wc.message.custom.send.post(body=content)
68 | ```
69 |
70 | 下载媒体文件,如何判断token失效:
71 | API URL:http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
72 | ```
73 | # 这里演示了如何捕获token失效的异常,产生这个异常就要更新token了
74 | try:
75 | rm = wc.media.get.file(media_id=rjson.media_id)
76 | print rm
77 | open('./test.png', 'rb').write(rm.read())
78 | rm.close()
79 | except AccessTokenError, e:
80 | print e
81 | #更新token
82 | wc.refurbish_access_token()
83 |
84 | # 另外我们可以主动判断token是否失效, 这里只是在时间上验证,大多数的情况下是能正常工作的
85 | if wc.is_expires():
86 | wc.refurbish_access_token()
87 | ```
88 |
89 | 微信智能接口
90 | ```
91 | q = '{"query":"查一下鹰潭的天气", "city":"鹰潭", "category":"weather", "appid":"%s"}' %(your_pid)
92 | print wc.semantic.semproxy.search.post(body=q)
93 | ```
94 |
95 | 媒体文件上传
96 | ```
97 | print wc.media.upload.file(type='video', mpeg4=open('./a.mp4', 'rb'))
98 | print wc.media.upload.file(type='image', jpeg=open('./a.jpg', 'rb'))
99 | print wc.media.upload.file(type='image', gif=open('./a.gif', 'rb'))
100 | print wc.media.upload.file(type='voice', amr=open('./a.amr', 'rb'))
101 | print wc.media.upload.file(type='voice', wma=open('./a.wma', 'rb'))
102 | ```
103 |
104 | Have Fun!
105 |
106 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from distutils.core import setup
2 | import sys
3 |
4 | import weixin
5 |
6 | kw = dict(
7 | name = 'weixinpy',
8 | version = weixin.__version__,
9 | description = 'Python client SDK for Micro Message Public Platform API',
10 | long_description = open('README', 'r').read(),
11 | author = 'Liang Cha',
12 | author_email = 'ckmx945@gmail.com',
13 | url = 'https://github.com/kun945/weixinpy',
14 | download_url = 'https://github.com/kun945/weixinpy',
15 | py_modules = ['weixin'],
16 | classifiers = [
17 | 'Development Status :: 5 - Production/Stable',
18 | 'Environment :: Web Environment',
19 | 'Intended Audience :: Developers',
20 | 'License :: OSI Approved :: Apache Software License',
21 | 'Operating System :: OS Independent',
22 | 'Programming Language :: Python',
23 | 'Topic :: Internet',
24 | 'Topic :: Software Development :: Libraries :: Python Modules',
25 | ])
26 |
27 | setup(**kw)
28 |
--------------------------------------------------------------------------------
/test/test.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | from weixin import WeiXinClient
5 |
6 | if __name__ == '__main__':
7 | wc = WeiXinClient('your_appid', 'your_secret', fc = True, path='/tmp')
8 | #"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET"
9 | wc.request_access_token()
10 |
11 | data = '{"touser":"obMnLt3bf7t65jyEsa7vOtXphdu4", "msgtype":"text", "text":{ "content":"hello!"}}'
12 | #data = '{"touser":"obMnLt9Qx5ZfPdElO3DQblM7ksl0", "msgtype":"image", ' \
13 | # '"image":{ "media_id":"OaPSe4DP-HF4s_ABWHEVDgMKOPCUoViID8x-yPUvwCfqTEA0whZOza4hGODiHs93"}}'
14 | key = '{"button":[{"type":"click","name":"test","key":"V1001_TEST"}]}'
15 | #"https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN"
16 | print wc.message.custom.send.post(body=data)
17 | #"https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN"
18 | print wc.user.info.dget(openid='obMnLt43lgfeeC8Ljn4-cLixEW6Q', lang='zh_CN')
19 | #"https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN"
20 | print wc.message.custom.send.post(body=data)
21 | #"https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID"
22 | print wc.user.get.dget(next_openid=None)
23 | #"http://file.api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE"
24 | print wc.media.upload.file(type='image', jpeg = open('./test.jpg', 'rb'))
25 | #"https://api.weixin.qq.com/cgi-bin/groups/create?access_token=ACCESS_TOKEN"
26 | print wc.groups.create.post(body='{"group":{"name":"test_group_01"}}')
27 | #"https://api.weixin.qq.com/cgi-bin/groups/update?access_token=ACCESS_TOKEN"
28 | print wc.groups.update.post(body='{"group":{"id":100,"name":"test"}}')
29 | #"https://api.weixin.qq.com/cgi-bin/groups/members/update?access_token=ACCESS_TOKEN"
30 | print wc.groups.members.update.post(body='{"openid":"obMnLt9Qx5ZfPdElO3DQblM7ksl0","to_groupid":100}')
31 | #"https://api.weixin.qq.com/cgi-bin/groups/getid?access_token=ACCESS_TOKEN"
32 | print wc.groups.getid.post(body='{"openid":"obMnLt9Qx5ZfPdElO3DQblM7ksl0"}')
33 | #"https://api.weixin.qq.com/cgi-bin/groups/get?access_token=ACCESS_TOKEN"
34 | print wc.groups.get.dget()
35 | #"https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN"
36 | print wc.menu.create.post(body=key)
37 | #"https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN"
38 | print wc.menu.get.dget()
39 | #"http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID"
40 | print wc.media.get.file(media_id='OaPSe4DP-HF4s_ABWHEVDgMKOPCUoViID8x-yPUvwCfqTEA0whZOza4hGODiHs93')
41 |
--------------------------------------------------------------------------------
/weixin.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import time
5 | import json
6 | import urllib
7 | import urllib2
8 |
9 | __version__ = '0.5.0'
10 | __author__ = 'Liang Cha (ckmx945@gmail.com)'
11 |
12 |
13 | '''
14 | Python client SDK for Micro Message Public Platform API.
15 | '''
16 |
17 |
18 | try:
19 | import memcache
20 | except Exception, e:
21 | print '\033[95mWrining: %s. use local FileCache.\033[0m' %e
22 |
23 |
24 | (_HTTP_GET, _HTTP_POST, _HTTP_FILE) = range(3)
25 |
26 | _CONTENT_TYPE_MEDIA = ('image/jpeg', 'audio/amr', 'video/mpeg4')
27 |
28 | _MEIDA_TYPE = ('mpeg4', 'jpeg', 'jpg', 'gif', 'png', 'bmp', 'mp3', 'wav', 'wma', 'amr')
29 |
30 | _CONTENT_TYPE_JSON= (
31 | 'application/json; encoding=utf-8',
32 | 'application/json',
33 | 'text/plain',
34 | )
35 |
36 | _API_URLS = (
37 | 'https://api.weixin.qq.com/',
38 | 'https://api.weixin.qq.com/cgi-bin/',
39 | )
40 |
41 | _OTHER_FEATURES = (
42 | 'semantic', #微信智能接口
43 | 'shackearound', #微信摇一摇周边
44 | 'sns', #网页授权相关
45 | 'datacube', #数据统计
46 | 'merchant', #微信小店
47 | )
48 |
49 |
50 | class APIError(StandardError):
51 | '''
52 | raise APIError if reciving json message indicating failure.
53 | '''
54 |
55 | def __init__(self, error_code, error_msg):
56 | self.error_code = error_code
57 | self.error_msg = error_msg
58 | StandardError.__init__(self, error_msg)
59 |
60 | def __str__(self):
61 | return '%d:%s' %(self.error_code, self.error_msg)
62 |
63 |
64 | class AccessTokenError(APIError):
65 | '''
66 | raise AccessTokenError if reciving json message indicating failure.
67 | '''
68 |
69 | def __init__(self, error_code, error_msg):
70 | APIError.__init__(self, error_code, error_msg)
71 |
72 | def __str__(self):
73 | return APIError.__str__(self)
74 |
75 |
76 | class JsonDict(dict):
77 | '''
78 | general json object that allows attributes to bound to and also behaves like a dict
79 | '''
80 |
81 | def __getattr__(self, attr):
82 | try:
83 | return self[attr]
84 | except KeyError:
85 | raise AttributeError(r"'JsonDict' object has no attribute '%s'" %(attr))
86 |
87 | def __setattr__(self, attr, value):
88 | self[attr] = value
89 |
90 |
91 | def _parse_json(s):
92 | ''' parse str into JsonDict '''
93 |
94 | def _obj_hook(pairs):
95 | o = JsonDict()
96 | for k, v in pairs.iteritems():
97 | o[str(k)] = v
98 | return o
99 | return json.loads(s, object_hook = _obj_hook)
100 |
101 |
102 | def _encode_params(**kw):
103 | '''
104 | do url-encode parmeters
105 |
106 | >>> _encode_params(a=1, b='R&D')
107 | 'a=1&b=R%26D'
108 | '''
109 | args = []
110 | body = None
111 | path = None
112 | for k, v in kw.iteritems():
113 | if k == 'body':
114 | body = v
115 | continue
116 | if k in _MEIDA_TYPE:
117 | continue
118 | if isinstance(v, basestring):
119 | qv = v.encode('utf-8') if isinstance(v, unicode) else v
120 | args.append('%s=%s' %(k, urllib.quote(qv)))
121 | else:
122 | if v is None:
123 | args.append('%s=' %(k))
124 | else:
125 | qv = str(v)
126 | args.append('%s=%s' %(k, urllib.quote(qv)))
127 | return ('&'.join(args), body)
128 |
129 |
130 | def _encode_multipart(**kw):
131 | '''
132 | build a multipart/form-data body with randomly generated boundary
133 | '''
134 | data = []
135 | boundary = '----------%s' % hex(int(time.time()) * 1000)
136 | media_key = [key for key in _MEIDA_TYPE if key in kw.keys()]
137 | media_key = media_key[0] if media_key else 'null'
138 | fd = kw.get(media_key)
139 | media_type = kw.get('type') if kw.has_key('type') else 'null'
140 | content = fd.read() if hasattr(fd, 'read') else 'null'
141 | filename = getattr(fd, 'name', None)
142 | if filename is None: filename = '/tmp/fake.%s' %(media_key)
143 | data.append('--%s' % boundary)
144 | data.append('Content-Disposition: form-data; name="%s"; filename="%s"' %(media_key, filename))
145 | data.append('Content-Length: %d' % len(content))
146 | data.append('Content-Type: %s/%s' %(media_type, media_key))
147 | data.append('Content-Transfer-Encoding: binary\r\n')
148 | data.append(content)
149 | data.append('--%s--\r\n' % boundary)
150 | if hasattr(fd, 'close'): fd.close()
151 | return '\r\n'.join(data), boundary
152 |
153 |
154 | class WeiXinResponse(object):
155 | ''' To deal with response of the base class '''
156 |
157 | def __init__(self, resp):
158 | self._resp = resp
159 |
160 | def read(self):
161 | return self._resp.read()
162 |
163 | def close(self):
164 | self._resp.close()
165 |
166 | def __str__(self):
167 | return self._resp.headers['Content-Type']
168 |
169 |
170 | class WeiXinJson(WeiXinResponse):
171 | ''' Json string '''
172 |
173 | def __init__(self, resp):
174 | WeiXinResponse.__init__(self, resp)
175 |
176 | def read(self):
177 | '''Check api or token error and return json'''
178 | rjson = _parse_json(self._resp.read())
179 | self._resp.close()
180 | if hasattr(rjson, 'errcode') and rjson.errcode != 0:
181 | if rjson.errcode in (40001, 40014, 41001, 42001):
182 | raise AccessTokenError(rjson.errcode, rjson.errmsg)
183 | raise APIError(rjson.errcode, rjson.errmsg)
184 | return rjson
185 |
186 | #response object close in read()
187 | def close(self):
188 | pass
189 |
190 |
191 | class WeiXinMedia(WeiXinResponse):
192 | ''' Audio, Image, Video etc... '''
193 |
194 | def __init__(self, resp):
195 | WeiXinResponse.__init__(self, resp)
196 |
197 |
198 | def _http_call(the_url, method, token, **kw):
199 | '''
200 | send an http request and return a json object if no error occurred.
201 | '''
202 | body = None
203 | params = None
204 | boundary = None
205 | (params, body) = _encode_params(**kw)
206 | if method == _HTTP_FILE:
207 | the_url = the_url.replace('https://api.', 'http://file.api.')
208 | body, boundary = _encode_multipart(**kw)
209 | if token == None:
210 | http_url = '%s?%s' %(the_url, params)
211 | else:
212 | the_url = '%s?access_token=%s' %(the_url, token)
213 | http_url = '%s&%s' %(the_url, params) if (method == _HTTP_GET or method == _HTTP_FILE) else the_url
214 | http_body = str(body) if (method == _HTTP_POST) else body
215 | req = urllib2.Request(http_url, data = http_body)
216 | if boundary: req.add_header('Content-Type', 'multipart/form-data; boundary=%s' % boundary)
217 | try:
218 | resp = urllib2.urlopen(req, timeout = 8)
219 | except urllib2.HTTPError, e:
220 | if resp.headers['Content-Type'] == _CONTENT_TYPE_JSON:
221 | json = WeiXinJson(resp)
222 | return json.read()
223 | except Exception, e:
224 | raise e
225 | try:
226 | content_type = resp.headers['Content-Type']
227 | except KeyError, e:
228 | content_type = '??'
229 | resp.headers['Content_Type'] = content_type
230 | #print content_type
231 | if content_type in _CONTENT_TYPE_MEDIA:
232 | return WeiXinMedia(resp)
233 | elif content_type in _CONTENT_TYPE_JSON:
234 | json = WeiXinJson(resp)
235 | return json.read()
236 | else:
237 | return WeiXinResponse(resp)
238 |
239 |
240 | class FileCache(object):
241 | '''
242 | the information is temporarily saved to the file.
243 | '''
244 |
245 | def __init__(self, path):
246 | self.path = path
247 | try:
248 | fd = open(self.path, 'rb'); data = fd.read(); fd.close()
249 | self.dict_data = json.loads(data)
250 | except Exception, e:
251 | self.dict_data = dict()
252 |
253 | def get(self, key):
254 | return self.dict_data.get(key)
255 |
256 | def set(self, key, value, time = 0):
257 | self.dict_data[key] = value
258 |
259 | def delete(self, key, time = 0):
260 | if self.dict_data.has_key(key): del self.dict_data[key]
261 |
262 | def save(self):
263 | data = (repr(self.dict_data)).replace('\'', '\"') #json must to use double quotation marks
264 | fd = open(self.path, 'wb'); fd.write(data); fd.close()
265 |
266 | def remove(self):
267 | import os
268 | try:
269 | os.remove(self.path)
270 | except Exception, e:
271 | pass
272 |
273 | def __str__(self):
274 | return repr(self.dict_data)
275 |
276 |
277 | class WeiXinClient(object):
278 | '''
279 | API clinet using synchronized invocation.
280 |
281 | >>> fc = True
282 | 'use memcache save access_token, otherwise use FileCache, path=[file_path | ip_addr]'
283 | '''
284 | def __init__(self, appID, appsecret, fc = True, path = '/tmp'):
285 | self.api_url= ''
286 | self.app_id = appID
287 | self.app_secret = appsecret
288 | self.access_token = None
289 | self.expires = 0
290 | self.fc = fc
291 | self.mc = FileCache('%s/access_token' %(path)) \
292 | if fc else memcache.Client([path], debug=0)
293 |
294 | def request_access_token(self):
295 | token_key = 'access_token_%s' %(self.app_id)
296 | expires_key = 'expires_%s' %(self.app_id)
297 | access_token = self.mc.get(token_key)
298 | expires = self.mc.get(expires_key)
299 | if not access_token or not expires or expires < int(time.time()):
300 | rjson =_http_call(_API_URLS[1] + 'token', _HTTP_GET,
301 | None, grant_type = 'client_credential',
302 | appid = self.app_id, secret = self.app_secret)
303 | self.access_token = str(rjson.access_token)
304 | self.expires = int(time.time()) + rjson.expires_in
305 | self.mc.set(token_key, self.access_token, time = rjson.expires_in)
306 | self.mc.set(expires_key, self.expires, time = rjson.expires_in)
307 | if self.fc: self.mc.save()
308 | else:
309 | self.access_token = str(access_token)
310 | self.expires = expires
311 |
312 | def del_access_token(self):
313 | token_key = 'access_token_%s' %(self.app_id)
314 | expires_key = 'expires_%s' %(self.app_id)
315 | self.access_token = None
316 | self.expires = 0
317 | self.mc.delete(token_key)
318 | self.mc.delete(expires_key)
319 |
320 | def refurbish_access_token(self):
321 | self.del_access_token()
322 | self.request_access_token()
323 |
324 | def set_access_token(self, token, expires_in, persistence=False):
325 | self.access_token = token
326 | self.expires = expires_in + int(time.time())
327 | if persistence:
328 | token_key = 'access_token_%s' %(self.app_id)
329 | expires_key = 'expires_%s' %(self.app_id)
330 | self.mc.set(token_key, token, time = expires_in)
331 | self.mc.set(expires_key, self.expires, time = expires_in)
332 | if self.fc: self.mc.save()
333 |
334 | def is_expires(self):
335 | return not self.access_token or int(time.time()) >= (self.expires - 10)
336 |
337 | def __getattr__(self, attr):
338 | self.api_url = _API_URLS[0] if attr in _OTHER_FEATURES else _API_URLS[1]
339 | return _Callable(self, attr)
340 |
341 | def __str__(self):
342 | return 'url=%s\napp_id=%s\napp_secret=%s\naccess_token=%s\nexpires=%d' \
343 | %(self.api_url, self.app_id, self.app_secret, self.access_token, self.expires)
344 |
345 |
346 | class _Executable(object):
347 |
348 | def __init__(self, client, method, path):
349 | self._client = client
350 | self._method = method
351 | self._path = path
352 |
353 | def __call__(self, **kw):
354 | return _http_call('%s%s' %(self._client.api_url, self._path), \
355 | self._method, self._client.access_token, **kw)
356 |
357 | def __str__(self):
358 | return '_Executable (%s)' %(self._path)
359 |
360 | __repr__ = __str__
361 |
362 |
363 |
364 | class _Callable(object):
365 |
366 | def __init__(self, client, name):
367 | self._client = client
368 | self._name = name
369 |
370 | def __getattr__(self, attr):
371 | if attr in ('dget', '_get'):
372 | return _Executable(self._client, _HTTP_GET, self._name)
373 | if attr == 'post':
374 | return _Executable(self._client, _HTTP_POST, self._name)
375 | if attr == 'file':
376 | return _Executable(self._client, _HTTP_FILE, self._name)
377 | name = '%s/%s' %(self._name, attr)
378 | return _Callable(self._client, name)
379 |
380 | def __str__(self):
381 | return '_Callable (%s)' %(self._name)
382 |
383 | def test():
384 | ''' test the API '''
385 | pass
386 |
387 | if __name__ == '__main__':
388 | test()
389 |
--------------------------------------------------------------------------------