新浪微博API OAuth 2 Python客户端

来源:互联网 发布:apache auth身份认证 编辑:程序博客网 时间:2024/05/11 18:51

最新源码

https://github.com/michaelliao/sinaweibopy/blob/master/weibo.py

源码如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
__version__ = '1.1.4'
__author__ = 'Liao Xuefeng (askxuefeng@gmail.com)'
'''
Python client SDK for sina weibo API using OAuth 2.
'''
try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO
import gzip, time, json, hmac, base64, hashlib, urllib, urllib2, logging, mimetypes, collections
class APIError(StandardError):
    '''
raise APIError if receiving json message indicating failure.
'''
    def __init__(self, error_code, error, request):
        self.error_code = error_code
        self.error = error
        self.request = request
        StandardError.__init__(self, error)
    def __str__(self):
        return 'APIError: %s: %s, request: %s' % (self.error_code, self.error, self.request)
def _parse_json(s):
    ' parse str into JsonDict '
    def _obj_hook(pairs):
        ' convert json object to python object '
        o = JsonDict()
        for k, v in pairs.iteritems():
            o[str(k)] = v
        return o
    return json.loads(s, object_hook=_obj_hook)
class JsonDict(dict):
    ' general json object that allows attributes to be bound to and also behaves like a dict '
    def __getattr__(self, attr):
        try:
            return self[attr]
        except KeyError:
            raise AttributeError(r"'JsonDict' object has no attribute '%s'" % attr)
    def __setattr__(self, attr, value):
        self[attr] = value
def _encode_params(**kw):
    '''
do url-encode parameters
>>> _encode_params(a=1, b='R&D')
'a=1&b=R%26D'
>>> _encode_params(a=u'\u4e2d\u6587', b=['A', 'B', 123])
'a=%E4%B8%AD%E6%96%87&b=A&b=B&b=123'
'''
    args = []
    for k, v in kw.iteritems():
        if isinstance(v, basestring):
            qv = v.encode('utf-8') if isinstance(v, unicode) else v
            args.append('%s=%s' % (k, urllib.quote(qv)))
        elif isinstance(v, collections.Iterable):
            for i in v:
                qv = i.encode('utf-8') if isinstance(i, unicode) else str(i)
                args.append('%s=%s' % (k, urllib.quote(qv)))
        else:
            qv = str(v)
            args.append('%s=%s' % (k, urllib.quote(qv)))
    return '&'.join(args)
def _encode_multipart(**kw):
    ' build a multipart/form-data body with randomly generated boundary '
    boundary = '----------%s' % hex(int(time.time() * 1000))
    data = []
    for k, v in kw.iteritems():
        data.append('--%s' % boundary)
        if hasattr(v, 'read'):
            # file-like object:
            filename = getattr(v, 'name', '')
            content = v.read()
            data.append('Content-Disposition: form-data; name="%s"; filename="hidden"' % k)
            data.append('Content-Length: %d' % len(content))
            data.append('Content-Type: %s\r\n' % _guess_content_type(filename))
            data.append(content)
        else:
            data.append('Content-Disposition: form-data; name="%s"\r\n' % k)
            data.append(v.encode('utf-8') if isinstance(v, unicode) else v)
    data.append('--%s--\r\n' % boundary)
    return '\r\n'.join(data), boundary
def _guess_content_type(url):
    n = url.rfind('.')
    if n==(-1):
        return 'application/octet-stream'
    ext = url[n:]
    return mimetypes.types_map.get(ext, 'application/octet-stream')
_HTTP_GET = 0
_HTTP_POST = 1
_HTTP_UPLOAD = 2
def _http_get(url, authorization=None, **kw):
    logging.info('GET %s' % url)
    return _http_call(url, _HTTP_GET, authorization, **kw)
def _http_post(url, authorization=None, **kw):
    logging.info('POST %s' % url)
    return _http_call(url, _HTTP_POST, authorization, **kw)
def _http_upload(url, authorization=None, **kw):
    logging.info('MULTIPART POST %s' % url)
    return _http_call(url, _HTTP_UPLOAD, authorization, **kw)
def _read_body(obj):
    using_gzip = obj.headers.get('Content-Encoding', '')=='gzip'
    body = obj.read()
    if using_gzip:
        gzipper = gzip.GzipFile(fileobj=StringIO(body))
        fcontent = gzipper.read()
        gzipper.close()
        return fcontent
    return body
def _http_call(the_url, method, authorization, **kw):
    '''
send an http request and return a json object if no error occurred.
'''
    params = None
    boundary = None
    if method==_HTTP_UPLOAD:
        # fix sina upload url:
        the_url = the_url.replace('https://api.', 'https://upload.api.')
        params, boundary = _encode_multipart(**kw)
    else:
        params = _encode_params(**kw)
        if '/remind/' in the_url:
            # fix sina remind api:
            the_url = the_url.replace('https://api.', 'https://rm.api.')
    http_url = '%s?%s' % (the_url, params) if method==_HTTP_GET else the_url
    http_body = None if method==_HTTP_GET else params
    req = urllib2.Request(http_url, data=http_body)
    req.add_header('Accept-Encoding', 'gzip')
    if authorization:
        req.add_header('Authorization', 'OAuth2 %s' % authorization)
    if boundary:
        req.add_header('Content-Type', 'multipart/form-data; boundary=%s' % boundary)
    try:
        resp = urllib2.urlopen(req, timeout=5)
        body = _read_body(resp)
        r = _parse_json(body)
        if hasattr(r, 'error_code'):
            raise APIError(r.error_code, r.get('error', ''), r.get('request', ''))
        return r
    except urllib2.HTTPError, e:
        try:
            r = _parse_json(_read_body(e))
        except:
            r = None
        if hasattr(r, 'error_code'):
            raise APIError(r.error_code, r.get('error', ''), r.get('request', ''))
        raise e
class HttpObject(object):
    def __init__(self, client, method):
        self.client = client
        self.method = method
    def __getattr__(self, attr):
        def wrap(**kw):
            if self.client.is_expires():
                raise APIError('21327', 'expired_token', attr)
            return _http_call('%s%s.json' % (self.client.api_url, attr.replace('__', '/')), self.method, self.client.access_token, **kw)
        return wrap
class APIClient(object):
    '''
API client using synchronized invocation.
'''
    def __init__(self, app_key, app_secret, redirect_uri=None, response_type='code', domain='api.weibo.com', version='2'):
        self.client_id = str(app_key)
        self.client_secret = str(app_secret)
        self.redirect_uri = redirect_uri
        self.response_type = response_type
        self.auth_url = 'https://%s/oauth2/' % domain
        self.api_url = 'https://%s/%s/' % (domain, version)
        self.access_token = None
        self.expires = 0.0
        self.get = HttpObject(self, _HTTP_GET)
        self.post = HttpObject(self, _HTTP_POST)
        self.upload = HttpObject(self, _HTTP_UPLOAD)
    def parse_signed_request(self, signed_request):
        '''
parse signed request when using in-site app.
Returns:
dict object like { 'uid': 12345, 'access_token': 'ABC123XYZ', 'expires': unix-timestamp },
or None if parse failed.
'''
        def _b64_normalize(s):
            appendix = '=' * (4 - len(s) % 4)
            return s.replace('-', '+').replace('_', '/') + appendix
        sr = str(signed_request)
        logging.info('parse signed request: %s' % sr)
        enc_sig, enc_payload = sr.split('.', 1)
        sig = base64.b64decode(_b64_normalize(enc_sig))
        data = _parse_json(base64.b64decode(_b64_normalize(enc_payload)))
        if data['algorithm'] != u'HMAC-SHA256':
            return None
        expected_sig = hmac.new(self.client_secret, enc_payload, hashlib.sha256).digest();
        if expected_sig==sig:
            data.user_id = data.uid = data.get('user_id', None)
            data.access_token = data.get('oauth_token', None)
            expires = data.get('expires', None)
            if expires:
                data.expires = data.expires_in = time.time() + expires
            return data
        return None
    def set_access_token(self, access_token, expires):
        self.access_token = str(access_token)
        self.expires = float(expires)
    def get_authorize_url(self, redirect_uri=None, **kw):
        '''
return the authorization url that the user should be redirected to.
'''
        redirect = redirect_uri if redirect_uri else self.redirect_uri
        if not redirect:
            raise APIError('21305', 'Parameter absent: redirect_uri', 'OAuth2 request')
        response_type = kw.pop('response_type', 'code')
        return '%s%s?%s' % (self.auth_url, 'authorize', \
                _encode_params(client_id = self.client_id, \
                        response_type = response_type, \
                        redirect_uri = redirect, **kw))
                        
    def _parse_access_token(self, r):
        '''
new:return access token as a JsonDict: {"access_token":"your-access-token","expires_in":12345678,"uid":1234}, expires_in is represented using standard unix-epoch-time
'''
        current = int(time.time())
        expires = r.expires_in + current
        remind_in = r.get('remind_in', None)
        if remind_in:
            rtime = int(remind_in) + current
            if rtime < expires:
                expires = rtime
        return JsonDict(access_token=r.access_token, expires=expires, expires_in=expires, uid=r.get('uid', None))
        
    def request_access_token(self, code, redirect_uri=None):
        redirect = redirect_uri if redirect_uri else self.redirect_uri
        if not redirect:
            raise APIError('21305', 'Parameter absent: redirect_uri', 'OAuth2 request')
        r = _http_post('%s%s' % (self.auth_url, 'access_token'), \
                client_id = self.client_id, \
                client_secret = self.client_secret, \
                redirect_uri = redirect, \
                code = code, grant_type = 'authorization_code')
        return self._parse_access_token(r)
    def refresh_token(self, refresh_token):
        req_str = '%s%s' % (self.auth_url, 'access_token')
        r = _http_post(req_str, \
            client_id = self.client_id, \
            client_secret = self.client_secret, \
            refresh_token = refresh_token, \
            grant_type = 'refresh_token')
        return self._parse_access_token(r)
    def is_expires(self):
        return not self.access_token or time.time() > self.expires
    def __getattr__(self, attr):
        if '__' in attr:
            return getattr(self.get, attr)
        return _Callable(self, attr)
_METHOD_MAP = { 'GET': _HTTP_GET, 'POST': _HTTP_POST, 'UPLOAD': _HTTP_UPLOAD }
class _Executable(object):
    def __init__(self, client, method, path):
        self._client = client
        self._method = method
        self._path = path
    def __call__(self, **kw):
        method = _METHOD_MAP[self._method]
        if method==_HTTP_POST and 'pic' in kw:
            method = _HTTP_UPLOAD
        return _http_call('%s%s.json' % (self._client.api_url, self._path), method, self._client.access_token, **kw)
    def __str__(self):
        return '_Executable (%s %s)' % (self._method, self._path)
    __repr__ = __str__
class _Callable(object):
    def __init__(self, client, name):
        self._client = client
        self._name = name
    def __getattr__(self, attr):
        if attr=='get':
            return _Executable(self._client, 'GET', self._name)
        if attr=='post':
            return _Executable(self._client, 'POST', self._name)
        name = '%s/%s' % (self._name, attr)
        return _Callable(self._client, name)
    def __str__(self):
        return '_Callable (%s)' % self._name
    __repr__ = __str__
if __name__=='__main__':
    import doctest
    doctest.testmod()


使用简介

注册微博App后,可以获得app key和app secret,然后定义网站回调地址:

from weibo import APIClientAPP_KEY = '1234567' # app keyAPP_SECRET = 'abcdefghijklmn' # app secretCALLBACK_URL = 'http://www.example.com/callback' # callback url

在网站放置“使用微博账号登录”的链接,当用户点击链接后,引导用户跳转至如下地址:

client = APIClient(app_key=APP_KEY, app_secret=APP_SECRET, redirect_uri=CALLBACK_URL)url = client.get_authorize_url()# TODO: redirect to url

用户授权后,将跳转至网站回调地址,并附加参数code=abcd1234:

# 获取URL参数code:code = your.web.framework.request.get('code')client = APIClient(app_key=APP_KEY, app_secret=APP_SECRET, redirect_uri=CALLBACK_URL)r = client.request_access_token(code)access_token = r.access_token # 新浪返回的token,类似abc123xyz456expires_in = r.expires_in # token过期的UNIX时间:http://zh.wikipedia.org/wiki/UNIX%E6%97%B6%E9%97%B4# TODO: 在此可保存access tokenclient.set_access_token(access_token, expires_in)

然后,可调用任意API:

print client.statuses.user_timeline.get()print client.statuses.update.post(status=u'测试OAuth 2.0发微博')print client.statuses.upload.post(status=u'测试OAuth 2.0带图片发微博', pic=open('/Users/michael/test.png'))

API调用规则

首先查看新浪微博API文档,例如:

API:statuses/user_timeline

请求格式:GET

请求参数:

source:string,采用OAuth授权方式不需要此参数,其他授权方式为必填参数,数值为应用的AppKey?。

access_token:string,采用OAuth授权方式为必填参数,其他授权方式不需要此参数,OAuth授权后获得。

uid:int64,需要查询的用户ID。

screen_name:string,需要查询的用户昵称。

(其它可选参数略)

调用方法:将API的“/”变为“.”,根据请求格式是GET或POST,调用get()或post()并传入关键字参数,但不包括source和access_token参数:

r = client.statuses.user_timeline.get(uid=123456)for st in r.statuses:    print st.text

若为POST调用,则示例代码如下:

r = client.statuses.update.post(status=u'测试OAuth 2.0发微博')

若需要上传文件,传入file-like object参数,示例代码如下:

f = open('/Users/michael/test.png', 'rb')r = client.statuses.upload.post(status=u'测试OAuth 2.0带图片发微博', pic=f)f.close() # APIClient不会自动关闭文件,需要手动关闭

请注意:上传的文件必须是file-like object,不能是str,因为无法区分一个str是文件还是字段。可以通过StringIO把一个str包装成file-like object。

站内应用

站内应用授权不能作URL跳转,而是由新浪微博POST用户信息至iFrame,需要在iFrame的页面中处理用户信息。

假定iFrame的入口地址是http://app.example.com/index,则新浪微博会POST数据至该URL。

第一步,获取POST数据:

signed_request = your.web.framework.get('signed_request')client = APIClient(APP_ID, APP_SECRET, 'http://app.example.com/callback')data = client.parse_signed_request(signed_request)

第二步,判断用户是否授权,如未授权,返回授权页面:

user_id = data.get('uid', '')auth_token = data.get('oauth_token', '')if not user_id or not auth_token:    return Template('auth.html')

授权页auth.html可以是静态页面,需要调用新浪的JS:

<html><head>    <title>未授权时的页面</title>    <script src="http://tjs.sjs.sinajs.cn/t35/apps/opent/js/frames/client.js"></script></head><script>function authLoad() {    App.AuthDialog.show({        client_id: 'YOUR_CLIENT_ID',        redirect_uri: 'http://apps.weibo.com/YOUR_APP_ID',        height: 40});}</script><body onload="authLoad();"></body></html>

第三步,如果用户已授权,则可直接获得OAuth token:

user_id = data.get('uid', '')auth_token = data.get('oauth_token', '')if not user_id or not auth_token:    return Template('auth.html')expires = data.expiresclient.set_access_token(auth_token, expires)

现在,已获取到用户的auth_token和expires,可以按照API进行正常调用。

使用限制

仅支持Web方式调用,不支持口令方式验证。

补充说明

所有API调用均为动态调用,支持链式调用,需要根据新浪API文档由HTTP调用方式(GET,POST)决定APIClient的链式方法名(如statuses.user_timeline)和最后调用的方法是get()还是post(),以及关键字参数。

若调用出错,会抛出APIError异常,该异常包含error_code,error和request三个字段,与新浪返回的出错json对应。具体错误原因请查询新浪文档。

若HTTP响应出错(例如404),会抛出urllib2.HTTPError异常。

0 0
原创粉丝点击