diff --git a/.gitignore b/.gitignore index 14eed52..a0659c6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ chinaapi.egg-info/ *.egg tests_local/ extends/ -packages/ \ No newline at end of file +packages/ +local/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 75ac9c5..e835fa1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,6 @@ language: python python: - "2.7" # command to install dependencies -install: "pip install -r requirements.txt" +install: pip install -r requirements.txt && pip install -r requirements-test.txt # command to run tests -script: nosetests \ No newline at end of file +script: python setup.py test \ No newline at end of file diff --git a/HISTORY.rst b/HISTORY.rst index aecd529..ebff042 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,11 @@ Release History --------------- +0.8.6 (2014年03月12日) +++++++++++++++++++ +- 添加:ApiRequestError +- 移除:InvalidApiError,NotExistApi,MutexApiParametersError + 0.8.0 (2014年01月20日) ++++++++++++++++++ - 添加新浪微博web登录函数 diff --git a/README.rst b/README.rst index 124f0fd..6518705 100644 --- a/README.rst +++ b/README.rst @@ -1,25 +1,27 @@ ChinaAPI ======== +ChinaAPI是一个API库,使用Python语言编写。 + .. image:: https://travis-ci.org/smallcode/ChinaAPI.png :target: https://travis-ci.org/smallcode/ChinaAPI .. image:: https://badge.fury.io/py/chinaapi.png :target: http://badge.fury.io/py/chinaapi - -ChinaAPI是一个API库,使用Python编写。 - -目前国内的几大开放平台,有新浪微博,腾讯微博,淘宝,人人,豆瓣等。 -针对这几个平台,用Python语言编写的API库都比较独立,各具特色。 -但仔细分析,不难发现这些库存在大量可通用的模块,并可抽象出统一的调用接口。 -ChinaAPI就是为此目的而存在。 +支持 +---- +- 新浪微博 +- 腾讯微博 +- 淘宝 +- 人人 +- 豆瓣(OAuth2) 安装 ---- -项目地址:https://github.com/smallcode/ChinaAPI +---- +可以到项目所在地址下载:https://github.com/smallcode/ChinaAPI -最简单的安装方法: +或者直接用pip安装: .. code-block:: bash @@ -227,11 +229,6 @@ OAuth2调用规则:**斜杠(/)映射为点(.)** ---- -TODO: ------ - -- 添加OAuth2说明 - 感谢以下Python SDK的开发者们的贡献: ----------------------- diff --git a/chinaapi/__init__.py b/chinaapi/__init__.py index e5b0b07..76cf23a 100644 --- a/chinaapi/__init__.py +++ b/chinaapi/__init__.py @@ -2,6 +2,6 @@ __title__ = 'chinaapi' -__version__ = '0.8.5' +__version__ = '0.8.9' __author__ = 'smallcode (45945756@qq.com)' __license__ = 'Apache 2.0' \ No newline at end of file diff --git a/chinaapi/douban/open.py b/chinaapi/douban/open.py index 44f1108..71db212 100644 --- a/chinaapi/douban/open.py +++ b/chinaapi/douban/open.py @@ -1,15 +1,12 @@ # coding=utf-8 from chinaapi.exceptions import ApiResponseError -from chinaapi.open import OAuth2Base, Token, App +from chinaapi.open import * class OAuth2(OAuth2Base): AUTH_URL = 'https://www.douban.com/service/auth2/auth' TOKEN_URL = 'https://www.douban.com/service/auth2/token' - def __init__(self, app=App()): - super(OAuth2, self).__init__(app) - def _parse_token(self, response): r = response.json_dict() if 'code' in r and 'msg' in r: diff --git a/chinaapi/exceptions.py b/chinaapi/exceptions.py index 23b04f8..b7b2ca0 100644 --- a/chinaapi/exceptions.py +++ b/chinaapi/exceptions.py @@ -2,7 +2,7 @@ class ApiError(IOError): """ API异常 """ - def __init__(self, url, code, message, sub_code='', sub_message=''): + def __init__(self, url='', code=0, message='', sub_code=0, sub_message=''): self.url = url self.code = code self.message = message @@ -10,69 +10,44 @@ def __init__(self, url, code, message, sub_code='', sub_message=''): self.sub_message = sub_message super(ApiError, self).__init__(code, message) + @staticmethod + def format(code, message): + return u'[%s]: %s, ' % (code, message) if code or message else '' + def __str__(self): - if self.sub_code or self.sub_message: - return u'[{0}]: {1}, [{2}]: {3}, request: {4}'.format(str(self.code), self.message, str(self.sub_code), - self.sub_message, self.url) - return u'[{0}]: {1}, request: {2}'.format(str(self.code), self.message, self.url) + return u'%s%srequest: %s' % ( + self.format(self.code, self.message), self.format(self.sub_code, self.sub_message), self.url) -class ApiResponseError(ApiError): - """ 响应结果中包含的异常 """ +class ApiRequestError(ApiError): + def __init__(self, request, code, message, sub_code=0, sub_message=''): + self.request = request + super(ApiRequestError, self).__init__(self.get_url(), code, message, sub_code, sub_message) - def __init__(self, response, code, message, sub_code='', sub_message=''): - self.response = response - super(ApiResponseError, self).__init__(self.get_url(), code, message, sub_code, sub_message) + def is_multipart(self): + return 'multipart/form-data' in self.request.headers.get('Content-Type', '') def get_url(self): - request = self.response.request - if 'multipart/form-data' not in request.headers.get('Content-Type', '') and request.body: - return u'{0}?{1}'.format(self.response.url, self.response.request.body) - return self.response.url - - -class ApiResponseValueError(ApiResponseError, ValueError): - """ 解析响应结果时抛出的异常 """ - - def __init__(self, response, value_error): - super(ApiResponseValueError, self).__init__(response, response.status_code, - response.text if response.text else str(value_error)) - - -class InvalidApi(ApiError, ValueError): - """ 无效API """ + if not self.is_multipart() and self.request.body: + return u'%s?%s' % (self.request.url, self.request.body) + return self.request.url - def __init__(self, url, code=0, message='Invalid Api!'): - super(InvalidApi, self).__init__(url, code, message) +class ApiResponseError(ApiRequestError): + """ 响应结果中包含的异常 """ -class NotExistApi(ApiResponseError, ValueError): - """ 不存在API """ - - def __init__(self, response, code=0, message='Request Api not found!'): - if response.text: - message = response.text - if not code: - code = response.status_code - super(NotExistApi, self).__init__(response, code, message) - - -class MutexApiParameters(ApiError, ValueError): - """ 同时存在两个或两个以上互相排斥的参数 """ - - def __init__(self, key_list): - super(MutexApiParameters, self).__init__('', '', u'{0}参数只能选择其一'.format(','.join(key_list))) + def __init__(self, response, code=0, message='', sub_code=0, sub_message=''): + self.response = response + super(ApiResponseError, self).__init__(response.request, + code or response.status_code, + message or response.text, + sub_code, + sub_message) class OAuth2Error(ApiError): """ OAuth2异常 """ - def __init__(self, url, code, message): - super(OAuth2Error, self).__init__(url, code, message) - class MissingRedirectUri(OAuth2Error, ValueError): - """ 缺少 redirect_uri """ - - def __init__(self, url): - super(MissingRedirectUri, self).__init__(url, 'OAuth2 request', 'Parameter absent: redirect_uri') \ No newline at end of file + """ 缺少 redirect_uri """ \ No newline at end of file diff --git a/chinaapi/open.py b/chinaapi/open.py index d2dcb8d..2474c06 100644 --- a/chinaapi/open.py +++ b/chinaapi/open.py @@ -27,8 +27,7 @@ def __init__(self, access_token=None, expires_in=None, refresh_token=None, **kwa self.expired_at = None self.expires_in = expires_in self.refresh_token = refresh_token - for key, value in kwargs.items(): - setattr(self, key, value) + self._data = kwargs @staticmethod def _get_now(): @@ -48,6 +47,11 @@ def _set_expires_in(self, expires_in): def is_expires(self): return not self.access_token or (self.expired_at is not None and self._get_now()> self.expired_at) + def __getattr__(self, item): + if item in self._data: + return self._data[item] + raise AttributeError + class App(object): def __init__(self, key='', secret='', redirect_uri=''): @@ -134,13 +138,11 @@ def try_request(): return try_request() def __getattr__(self, attr): - if not attr.startswith('__'): - return ClientWrapper(self, attr) - raise AttributeError + return ClientWrapper(self, attr) class OAuthBase(Request): - def __init__(self, app): + def __init__(self, app=App()): super(OAuthBase, self).__init__() self.app = app @@ -149,9 +151,6 @@ class OAuth2Base(OAuthBase): AUTH_URL = '' TOKEN_URL = '' - def __init__(self, app): - super(OAuth2Base, self).__init__(app) - def _parse_token(self, response): return response.json_dict() diff --git a/chinaapi/qq/weibo/open.py b/chinaapi/qq/weibo/open.py index b56bc3d..250cb60 100644 --- a/chinaapi/qq/weibo/open.py +++ b/chinaapi/qq/weibo/open.py @@ -1,6 +1,6 @@ # coding=utf-8 from chinaapi.open import ClientBase, Method, OAuth2Base, Token, App -from chinaapi.exceptions import InvalidApi, ApiResponseError +from chinaapi.exceptions import ApiResponseError from chinaapi.utils import parse_querystring @@ -10,7 +10,7 @@ 't': lambda m: m in ['re_add', 'reply', 'comment', 'like', 'unlike'], 'fav': lambda m: m in ['addht', 'addt', 'delht', 'delt'], 'vote': lambda m: m in ['createvote', 'vote'], - 'list': lambda m: m != 'timeline', # 只有timeline接口是读接口,其他全是写接口 + 'list': lambda m: m != 'timeline', # 只有timeline接口是读接口,其他全是写接口 'lbs': lambda m: True # 全是写接口 } @@ -38,7 +38,7 @@ def parse(response): class Client(ClientBase): - #写接口 + # 写接口 _post_methods = ['add', 'del', 'create', 'delete', 'update', 'upload'] def __init__(self, app=App(), token=Token(), openid=None, clientip=None): @@ -53,13 +53,11 @@ def _prepare_url(self, segments, queries): """ 因del为Python保留字,无法作为方法名,需将del替换为delete,并在此处进行反向转换。 """ - if segments[-1] == 'delete' and segments[-2] != 'list': # list本身有delete方法,需排除 - segments[-1] = 'del' + if len(segments) == 2 and segments[0] != 'list' and segments[1] == 'delete': # list本身有delete方法,需排除 + segments[1] = segments[1].replace('delete', 'del') return 'https://open.t.qq.com/api/{0}'.format('/'.join(segments)) def _prepare_method(self, segments): - if len(segments) != 2: - raise InvalidApi(self._prepare_url(segments, None)) model, method = tuple([segment.lower() for segment in segments]) if method.split('_')[0] in self._post_methods: return Method.POST @@ -81,9 +79,6 @@ class OAuth2(OAuth2Base): AUTH_URL = 'https://open.t.qq.com/cgi-bin/oauth2/authorize' TOKEN_URL = 'https://open.t.qq.com/cgi-bin/oauth2/access_token' - def __init__(self, app): - super(OAuth2, self).__init__(app) - def _parse_token(self, response): data = parse_querystring(response.text) if 'errorCode' in data: diff --git a/chinaapi/renren/open.py b/chinaapi/renren/open.py index df4bd1f..58e1cee 100644 --- a/chinaapi/renren/open.py +++ b/chinaapi/renren/open.py @@ -33,9 +33,6 @@ class OAuth2(OAuth2Base): AUTH_URL = 'https://graph.renren.com/oauth/authorize' TOKEN_URL = 'https://graph.renren.com/oauth/token' - def __init__(self, app): - super(OAuth2, self).__init__(app) - def _parse_token(self, response): r = response.json_dict() if 'error_code' in r: diff --git a/chinaapi/request.py b/chinaapi/request.py index 5b1e968..9ca07e5 100644 --- a/chinaapi/request.py +++ b/chinaapi/request.py @@ -1,21 +1,23 @@ # coding=utf-8 import types -import requests import re + +import requests + from .jsonDict import JsonDict, loads -from .exceptions import ApiResponseValueError, NotExistApi +from .exceptions import ApiResponseError def json_dict(self): try: return self.json(object_hook=lambda pairs: JsonDict(pairs.iteritems())) except ValueError, e: - if self.status_code == 200: - raise ApiResponseValueError(self, e) - elif 400 <= self.status_code < 500: - raise NotExistApi(self) - else: + try: self.raise_for_status() + except requests.RequestException, e: + raise ApiResponseError(self, message=u'%s, response: %s' % (e, self.text)) + else: + raise ApiResponseError(self, e.__class__.__name__, u'%s, value: %s' % (e, self.text)) def jsonp_dict(self): diff --git a/chinaapi/sina/weibo/open.py b/chinaapi/sina/weibo/open.py index 4a5e59f..35505c2 100644 --- a/chinaapi/sina/weibo/open.py +++ b/chinaapi/sina/weibo/open.py @@ -76,9 +76,6 @@ class OAuth2(OAuth2Base): AUTH_URL = BASE_URL + 'authorize' TOKEN_URL = BASE_URL + 'access_token' - def __init__(self, app): - super(OAuth2, self).__init__(app) - def _parse_token(self, response): data = parse(response) data['created_at'] = data.get('create_at', None) diff --git a/chinaapi/sina/weibo/web.py b/chinaapi/sina/weibo/web.py index 2f17bad..23459b1 100644 --- a/chinaapi/sina/weibo/web.py +++ b/chinaapi/sina/weibo/web.py @@ -12,7 +12,8 @@ class Client(ClientBase): JS_CLIENT = 'ssologin.js(v1.4.11)' LOGIN_URL = 'http://login.sina.com.cn/sso/login.php' - URL_REGEX = re.compile('replace\("(.*)"\)') + PRE_LOGIN_URL = 'http://login.sina.com.cn/sso/prelogin.php' + URL_REGEX = re.compile(r"replace\(['\"]+(.*)['\"]+\)") @staticmethod def encrypt_password(password, pre_data): @@ -30,7 +31,7 @@ def pre_login(self, su): 'rsakt': 'mod', # '_': time.time(), } - r = self._session.get('http://login.sina.com.cn/sso/prelogin.php', params=params) + r = self._session.get(self.PRE_LOGIN_URL, params=params) return r.jsonp_dict() def login(self, username, password): diff --git a/chinaapi/taobao/open.py b/chinaapi/taobao/open.py index 0d40ded..0d1167d 100644 --- a/chinaapi/taobao/open.py +++ b/chinaapi/taobao/open.py @@ -105,9 +105,6 @@ class OAuth2(OAuth2Base): AUTH_URL = 'https://oauth.taobao.com/authorize' TOKEN_URL = 'https://oauth.taobao.com/token' - def __init__(self, app): - super(OAuth2, self).__init__(app) - def _parse_token(self, response): data = parse(response) return Token(**data) @@ -125,9 +122,6 @@ class OAuth(OAuthBase): """ URL = 'http://container.open.taobao.com/container' - def __init__(self, app): - super(OAuth, self).__init__(app) - def _sign_by_md5(self, data): message = join_dict(data) + self.app.secret return md5(message).hexdigest().upper() diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..9523772 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,3 @@ +httpretty>=0.8.3 +vcrpy==0.6.0 +fake-factory \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 04db131..7811d60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ rsa -requests -httpretty==0.7.1 \ No newline at end of file +requests \ No newline at end of file diff --git a/setup.py b/setup.py index 13fa3a1..27137c8 100644 --- a/setup.py +++ b/setup.py @@ -20,11 +20,17 @@ 'chinaapi.netease', ] -requires = [ +install_requires = [ 'requests>= 2.1.0', 'rsa>= 3.1.2', ] +tests_require = [ + 'httpretty>= 0.8.3', + 'vcrpy>= 0.6.0', + 'fake-factory', +] + with open('README.rst') as f: readme = f.read() with open('HISTORY.rst') as f: @@ -41,7 +47,9 @@ packages=packages, package_dir={'chinaapi': 'chinaapi'}, include_package_data=True, - install_requires=requires, + install_requires=install_requires, + tests_require=tests_require, + test_suite="tests.get_tests", license=chinaapi.__license__, zip_safe=False, classifiers=[ diff --git a/tests/__init__.py b/tests/__init__.py index 9bad579..76322b8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,8 @@ # coding=utf-8 +import os.path +import unittest + + +def get_tests(): + start_dir = os.path.dirname(__file__) + return unittest.TestLoader().discover(start_dir, pattern="*.py") \ No newline at end of file diff --git a/tests/decorator.py b/tests/decorator.py new file mode 100644 index 0000000..1638746 --- /dev/null +++ b/tests/decorator.py @@ -0,0 +1,37 @@ +# coding=utf-8 +from functools import wraps +from vcr import VCR +import os + + +def use_cassette(vcr=VCR(), path='fixtures/vcr_cassettes/', **kwargs): + """ + Usage: + @use_cassette() # the default path will be fixtures/vcr_cassettes/foo.yaml + def foo(self): + ... + + @use_cassette(path='fixtures/vcr_cassettes/synopsis.yaml', record_mode='one') + def foo(self): + ... + """ + + def decorator(func): + @wraps(func) + def inner_func(*args, **kw): + if os.path.isabs(path): + file_path = path + else: + fun_path, fun_filename = os.path.split(func.func_code.co_filename) + file_path = os.path.join(fun_path, path, os.path.splitext(fun_filename)[0]) + + if not os.path.splitext(file_path)[1]: + serializer = kwargs.get('serializer', vcr.serializer) + file_path = os.path.join(file_path, '{0}.{1}'.format(func.func_name.lower(), serializer)) + + with vcr.use_cassette(file_path, **kwargs): + return func(*args, **kw) + + return inner_func + + return decorator \ No newline at end of file diff --git a/tests/images/pic.jpg b/tests/images/pic.jpg deleted file mode 100644 index 45dbc6f..0000000 Binary files a/tests/images/pic.jpg and /dev/null differ diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index bd520b2..4a90be4 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,6 +1,6 @@ # coding=utf-8 from unittest import TestCase -from chinaapi.exceptions import ApiError, MutexApiParameters +from chinaapi.exceptions import ApiError class ExceptionTest(TestCase): @@ -12,7 +12,3 @@ def test_api_error_with_sub(self): error = ApiError('http://request_url', 404, 'Request Api not found!', 1000, 'sub error msg') self.assertEqual('[404]: Request Api not found!, [1000]: sub error msg, request: http://request_url', str(error)) - - def test_mutex_api_parameters(self): - error = MutexApiParameters(['pic', 'pic_url']) - self.assertEqual(error.message, u'pic,pic_url参数只能选择其一') diff --git a/tests/test_open.py b/tests/test_open.py index 70badf5..6083bc1 100644 --- a/tests/test_open.py +++ b/tests/test_open.py @@ -7,9 +7,6 @@ class ApiClient(ClientBase): - def __init__(self, app): - super(ApiClient, self).__init__(app) - def _prepare_url(self, segments, queries): return BASE_URL + '/'.join(segments) @@ -29,17 +26,13 @@ def _is_retry_error(self, e): class NotImplementedClient(ClientBase): - def __init__(self, app): - super(NotImplementedClient, self).__init__(app) + pass class ApiOAuth2(OAuth2Base): AUTH_URL = 'http://test/oauth2/authorize' TOKEN_URL = 'http://test/oauth2/access_token' - def __init__(self, app): - super(ApiOAuth2, self).__init__(app) - class RequestBase(TestBase): def setUp(self): diff --git a/tests/test_qq_weibo.py b/tests/test_qq_weibo.py index 136362e..f05b248 100644 --- a/tests/test_qq_weibo.py +++ b/tests/test_qq_weibo.py @@ -1,14 +1,7 @@ # coding=utf-8 from unittest import TestCase from chinaapi.qq.weibo.open import Client, App -from chinaapi.exceptions import ApiError, NotExistApi, InvalidApi - - -# 返回text是unicode,设置默认编码为utf8 -import sys - -reload(sys) -sys.setdefaultencoding('utf8') +from chinaapi.exceptions import ApiError class QqWeiboTest(TestCase): @@ -32,14 +25,6 @@ def setUp(self): # r = self.client.t.upload_pic(pic=pic, pic_type=2, clientip='220.181.111.85') # clientip必填 # self.assertIsNotNone(r.imgurl) - def test_not_exist_api(self): - with self.assertRaises(NotExistApi): - self.client.not_exist_api.get() - - def test_invalid_api(self): - with self.assertRaises(InvalidApi): - self.client.too.many.segments.get() - def test_api_error(self): self.client.openid = '' with self.assertRaises(ApiError) as cm: diff --git a/tests/test_renren.py b/tests/test_renren.py index d5e458b..d46682f 100644 --- a/tests/test_renren.py +++ b/tests/test_renren.py @@ -1,7 +1,7 @@ # coding=utf-8 from unittest import TestCase from chinaapi.renren.open import Client, App -from chinaapi.exceptions import ApiError, NotExistApi +from chinaapi.exceptions import ApiError class RenRenTest(TestCase): @@ -24,7 +24,3 @@ def test_api_error(self): with self.assertRaises(ApiError) as cm: self.client.user.get(userId=self.uid) self.assertEqual(u'验证参数错误。', cm.exception.message) - - def test_not_exist_api(self): - with self.assertRaises(NotExistApi): - self.client.not_exist_api.get() diff --git a/tests/test_request.py b/tests/test_request.py index d0cec31..ce59bc7 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -1,8 +1,7 @@ # coding=utf-8 from unittest import TestCase -from requests import HTTPError from chinaapi.request import Request -from chinaapi.exceptions import NotExistApi, ApiResponseValueError +from chinaapi.exceptions import ApiResponseError import httpretty @@ -44,20 +43,20 @@ def test_jsonp_dict(self): @httpretty.activate def test_ApiResponseValueError(self): self._register_response('not json') - with self.assertRaises(ApiResponseValueError): + with self.assertRaises(ApiResponseError): response = self.session.post(self.URL) response.json_dict() @httpretty.activate def test_NotExistApi(self): self._register_response(status=404) - with self.assertRaises(NotExistApi): + with self.assertRaises(ApiResponseError): response = self.session.post(self.URL) response.json_dict() @httpretty.activate def test_HTTPError(self): self._register_response(status=500) - with self.assertRaises(HTTPError): + with self.assertRaises(ApiResponseError): response = self.session.post(self.URL) response.json_dict() diff --git a/tests/test_utils.py b/tests/test_utils.py index a0427d4..03c9825 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,9 +2,10 @@ from unittest import TestCase from chinaapi.utils import parse_querystring + class UtilsTest(TestCase): def test_parse_querystring(self): - r = parse_querystring("a=a&b=b") + r = parse_querystring("http://test.com?foo=bar&n=1") self.assertEqual(2, len(r)) - self.assertEqual('a', r['a']) - self.assertEqual('b', r['b']) \ No newline at end of file + self.assertEqual(r['foo'], 'bar') + self.assertEqual( r['n'], '1') \ No newline at end of file

AltStyle によって変換されたページ (->オリジナル) /