├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── MANIFEST.in ├── README.rst ├── bootstrap.py ├── buildout.cfg ├── setup.cfg ├── setup.py ├── src └── alipay │ ├── __init__.py │ ├── exceptions.py │ └── tests.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .installed.cfg 3 | .mr.developer.cfg 4 | bin/ 5 | develop-eggs/ 6 | parts/ 7 | *~ 8 | .ropeproject/ 9 | eggs 10 | *.egg-info/ 11 | .tox/ 12 | *.egg/ 13 | .idea/ 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | install: 6 | - "pip install ." 7 | script: nosetests 8 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ============================== 3 | 4 | 5 | 0.7.4 - Feb.28, 2017 6 | -------------------------------- 7 | 8 | - add `refund_fastpay_by_platform_pwd` method 9 | https://github.com/lxneng/alipay/pull/26 10 | 11 | 0.7.3 - Dec.14, 2015 12 | -------------------------------- 13 | 14 | - replace open() calls with io.open() for Python 3 compatibility, 15 | fix `UnicodeDecodeError` 16 | - add `create_direct_pay_by_user_url` doc for Wap site 17 | 18 | 19 | 0.7.2 - Nov.1, 2015 20 | -------------------------------- 21 | 22 | - add `single_trade_query` method 23 | https://github.com/lxneng/alipay/pull/20 24 | 25 | 0.7.1 - Sep.16, 2015 26 | -------------------------------- 27 | 28 | - Fix verify_notify raise KeyError: 'sign' bug 29 | https://github.com/lxneng/alipay/pull/18 30 | 31 | 0.7 - Sep.07, 2015 32 | -------------------------------- 33 | 34 | - add `create_forex_trade_url` method 35 | - add `create_forex_trade_wap_url` method 36 | - add `create_batch_trans_notify_url` method 37 | 38 | 0.6 - Jul.27, 2015 39 | -------------------------------- 40 | 41 | - add `send_goods_confirm_by_platform` method 42 | 43 | 0.5 - Apr.16, 2015 44 | -------------------------------- 45 | 46 | - add `add_alipay_qrcode` method 47 | 48 | 0.4.2 - Feb.14, 2015 49 | -------------------------------- 50 | 51 | - Fix argument type error of verify_notify in README 52 | 53 | - FIX SEVERE FAULT IN `check_notify_remotely` 54 | 55 | 56 | 0.4.1 - Feb.09, 2015 57 | -------------------------------- 58 | 59 | - Resolved README.rst is not formatted on pypi.python.org 60 | 61 | 0.4 - Feb.09, 2015 62 | -------------------------------- 63 | 64 | - Seller id support 65 | 66 | 67 | 0.3 - Aug.03, 2014 68 | -------------------------------- 69 | 70 | - Add wap payment support 71 | 72 | 0.2.3 - Nov.20, 2013 73 | -------------------------------- 74 | 75 | - english version readme doc 76 | 77 | 0.2.2 - Nov.12, 2013 78 | -------------------------------- 79 | 80 | - add includeme func for pyramid 81 | 82 | - update readme 83 | 84 | 0.2.1 - Nov.11, 2013 85 | -------------------------------- 86 | 87 | - fix rst doc 88 | 89 | 0.2 - Nov.11, 2013 90 | -------------------------------- 91 | 92 | - add unittest 93 | 94 | - update readme 95 | 96 | - add verify_notify func 97 | 98 | - add check_parameters func 99 | 100 | - add travis.yml 101 | 102 | - add tox.ini 103 | 104 | 0.1 - Nov.11, 2013 105 | ------------------------------ 106 | 107 | - first commit 108 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | An Unofficial Alipay API for Python 2 | ======================================= 3 | 4 | .. image:: https://img.shields.io/travis/lxneng/alipay.svg 5 | :target: https://travis-ci.org/lxneng/alipay 6 | 7 | .. image:: https://img.shields.io/pypi/v/alipay.svg 8 | :target: https://pypi.python.org/pypi/alipay/ 9 | 10 | .. image:: https://img.shields.io/pypi/dm/alipay.svg 11 | :target: https://pypi.python.org/pypi/alipay/ 12 | 13 | Overview 14 | --------------------------------------- 15 | 16 | An Unofficial Alipay API for Python, It Contain these API: 17 | 18 | - Generate direct payment url 19 | - Generate partner trade payment url 20 | - Generate standard mixed payment url 21 | - Generate batch trans pay url 22 | - Generate send goods confirm url 23 | - Generate forex trade url 24 | - Generate QR code url 25 | - Verify notify 26 | - Single Trade Query 27 | - Generate Refund With Pwd URL 28 | 29 | official document: https://b.alipay.com/order/techService.htm 30 | 31 | Install 32 | --------------------------------------- 33 | 34 | .. code-block:: bash 35 | 36 | pip install alipay 37 | 38 | Usage 39 | --------------------------------------- 40 | 41 | Initialization 42 | ~~~~~~~~~~~~~~~~~~~~~~~ 43 | 44 | .. code-block:: python 45 | 46 | >>> from alipay import Alipay 47 | >>> alipay = Alipay(pid='your_alipay_pid', key='your_alipay_key', seller_email='your_seller_mail') 48 | 49 | Or you can use `seller_id` instead of `seller_email`: 50 | 51 | .. code-block:: python 52 | 53 | >>> alipay = Alipay(pid='your_alipay_pid', key='your_alipay_key', seller_id='your_seller_id') 54 | 55 | 56 | Generate direct payment url 57 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | 59 | .. 60 | 61 | 生成即时到账支付链接 62 | 63 | Introduction: https://b.alipay.com/order/productDetail.htm?productId=2012111200373124 64 | 65 | .. code-block:: python 66 | 67 | >>> alipay.create_direct_pay_by_user_url(out_trade_no='your_order_id', subject='your_order_subject', total_fee='100.0', return_url='your_order 68 | _return_url', notify_url='your_order_notify_url') 69 | 'https://mapi.alipay.com/gateway.do?seller_email=.....' 70 | 71 | Generate partner trade payment url 72 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 73 | 74 | .. 75 | 76 | 生成担保交易支付链接 77 | 78 | Introduction: https://b.alipay.com/order/productDetail.htm?productId=2012111200373121 79 | 80 | .. code-block:: python 81 | 82 | >>> params = { 83 | ... 'out_trade_no': 'your_order_id', 84 | ... 'subject': 'your_order_subject', 85 | ... 'logistics_type': 'DIRECT', 86 | ... 'logistics_fee': '0', 87 | ... 'logistics_payment': 'SELLER_PAY', 88 | ... 'price': '10.00', 89 | ... 'quantity': '12', 90 | ... 'return_url': 'your_order_return_url', 91 | ... 'notify_url': 'your_order_notify_url' 92 | ... } 93 | >>> alipay.create_partner_trade_by_buyer_url(**params) 94 | 'https://mapi.alipay.com/gateway.do?seller_email=.....' 95 | 96 | Generate standard mixed payment url 97 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 98 | 99 | .. 100 | 101 | 生成标准双接口支付链接 102 | 103 | Introduction: https://b.alipay.com/order/productDetail.htm?productId=2012111300373136 104 | 105 | .. code-block:: python 106 | 107 | >>> alipay.trade_create_by_buyer_url(**params) 108 | 'https://mapi.alipay.com/gateway.do?seller_email=.....' 109 | 110 | Generate batch trans pay url 111 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 112 | 113 | .. 114 | 115 | 生成批量付款链接 116 | 117 | Introduction: https://b.alipay.com/order/productDetail.htm?productId=2012111200373121 118 | 119 | .. code-block:: python 120 | 121 | >>> params = { 122 | ... 'batch_list': (), #批量付款用户列表 123 | ... 'account_name': 'seller_account_name', #卖家支付宝名称 124 | ... 'batch_no': 'batch_id', #转账流水号,须唯一 125 | ... 'notify_url': 'your_batch_notify_url' #异步通知地址 126 | ... } 127 | >>> alipay.create_batch_trans_notify_url(**params) 128 | 'https://mapi.alipay.com/gateway.do?seller_email=xxx&detail_data=....' 129 | 130 | Note: batch_list 为批量付款用户列表,具体格式如下例子:(如涉及中文请使用unicode字符) 131 | 132 | .. code-block:: python 133 | 134 | >>> batch_list = ({'account': 'test@xxx.com', #支付宝账号 135 | ... 'name': u'测试', #支付宝用户姓名 136 | ... 'fee': '100', #转账金额 137 | ... 'note': 'test'}, 138 | ... {'account': 'test@xxx.com', #支付宝账号 139 | ... 'name': u'测试', #支付宝用户姓名 140 | ... 'fee': '100', #转账金额 141 | ... 'note': 'test'}) #转账原因 142 | 143 | Generate send goods confirm url 144 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 145 | 146 | .. 147 | 148 | 生成确认发货链接 149 | 150 | Introduction: https://cshall.alipay.com/support/help_detail.htm?help_id=491097 151 | 152 | .. code-block:: python 153 | 154 | >>> params = { 155 | ... 'trade_no': 'your_alipay_trade_id', 156 | ... 'logistics_name': 'your_logicstic_name', 157 | ... 'transport_type': 'EXPRESS', 158 | ... 'invocie_no': 'your_invocie_no' 159 | ... } 160 | >>> alipay.send_goods_confirm_by_platform(**params) 161 | 'https://mapi.alipay.com/gateway.do?sign=.....&trade_no=...' 162 | 163 | Generate forex trade url 164 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 165 | 166 | .. 167 | 168 | - Create website payment for foreigners (With QR code) 169 | - Create mobile payment for foreigners 170 | 171 | Introduction: http://global.alipay.com/ospay/home.htm 172 | 173 | .. code-block:: python 174 | 175 | >>> params = { 176 | ... 'out_trade_no': 'your_order_id', 177 | ... 'subject': 'your_order_subject', 178 | ... 'logistics_type': 'DIRECT', 179 | ... 'logistics_fee': '0', 180 | ... 'logistics_payment': 'SELLER_PAY', 181 | ... 'price': '10.00', 182 | ... 'quantity': '12', 183 | ... 'return_url': 'your_order_return_url', 184 | ... 'notify_url': 'your_order_notify_url' 185 | ... } 186 | >>> # Create website payment for foreigners 187 | >>> alipay.create_forex_trade_url(**params) 188 | 'https://mapi.alipay.com/gateway.do?service=create_forex_trade......' 189 | >>> # Create mobile payment for foreigners 190 | >>> alipay.create_forex_trade_wap_url(**params) 191 | 'https://mapi.alipay.com/gateway.do?service=create_forex_trade_wap......' 192 | 193 | 194 | Generate QR code url 195 | ~~~~~~~~~~~~~~~~~~~ 196 | 197 | .. 198 | 199 | 生成创建 QR 码链接 200 | 201 | Introduction: https://b.alipay.com/order/productDetail.htm?productId=2012120700377303 202 | 203 | .. code-block:: python 204 | 205 | >>> alipay.add_alipay_qrcode_url(**params) 206 | 'https://mapi.alipay.com/gateway.do?seller_id=.......' 207 | 208 | Note: 如果你的 `biz_data` 中有 Unicode 字符,在 dumps 的时候需要把 `ensure_ascii` 设置为 `False`,即 :code:`json.dumps(d, ensure_ascii=False)` 否则会遇到错误 209 | 210 | 211 | Verify notify 212 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 213 | 214 | verify notify from alipay server, example in Pyramid Application 215 | 216 | .. code-block:: python 217 | 218 | def alipy_notify(request): 219 | alipay = request.registry['alipay'] 220 | if alipay.verify_notify(**request.params): 221 | # this is a valid notify, code business logic here 222 | else: 223 | # this is a invalid notify 224 | 225 | 226 | Single Trade Query 227 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 228 | 229 | .. 230 | 231 | 单笔交易查询 232 | 233 | 文档:http://wenku.baidu.com/link?url=WLjyz-H6AlfDLIU7kR4LcVNQgxSTMxX61fW0tDCE8yZbqXflCd0CVFsZaIKbRFDvVLaFlq0Q3wcJ935A7Kw-mRSs0iA4wQu8cLaCe5B8FIq 234 | 235 | .. code-block:: python 236 | 237 | import re 238 | xml = alipay.single_trade_query(out_trade_no="10000005") 239 | res = re.findall('(\S+)', xml) # use RE to find trade_status, xml parsing is more useful, in fact. 240 | status = None if not res else res[0] 241 | print status # will print out TRADE_SUCCESS when trade is success 242 | 243 | Generate Refund With Pwd URL 244 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 245 | 246 | .. 247 | 248 | 生成即时到账有密退款链接 249 | 250 | Introduction: https://doc.open.alipay.com/docs/doc.htm?spm=a219a.7629140.0.0.XRddqH&treeId=62&articleId=104744&docType=1 251 | 252 | .. code-block:: python 253 | 254 | >>> params = { 255 | ... 'batch_list': (), #批量退款数据集 256 | ... 'batch_no': 'batch_id', #退款批次号,须唯一 257 | ... 'notify_url': 'your_batch_notify_url' #异步通知地址 258 | ... } 259 | >>> alipay.refund_fastpay_by_platform_pwd(**params) 260 | 'https://mapi.alipay.com/gateway.do?seller_email=xxx&detail_data=....' 261 | 262 | Note: batch_list 为批量退款数据集,具体格式如下例子:(如涉及中文请使用unicode字符) 263 | 264 | .. code-block:: python 265 | 266 | >>> batch_list = ({'trade_no': 'xxxxxxxx', #原付款支付宝交易号 267 | ... 'fee': '100', #退款总金额 268 | ... 'note': 'test'}, #退款原因 269 | ... {'trade_no': 'xxxxxxxx', #原付款支付宝交易号 270 | ... 'fee': '100', #退款总金额 271 | ... 'note': 'test'}) #退款原因 272 | 273 | Example in Pyramid Application 274 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 275 | 276 | Include alipay either by setting your includes in your .ini, or by calling config.include('alipay'). 277 | 278 | .. code-block:: python 279 | 280 | pyramid.includes = alipay 281 | 282 | now in your View 283 | 284 | .. code-block:: python 285 | 286 | def some_view(request): 287 | alipay = request.registry['alipay'] 288 | url = alipay.create_direct_pay_by_user_url(...) 289 | 290 | 291 | Reference 292 | --------------------------------------- 293 | 294 | - `Ruby Alipay GEM `_ 295 | - `Official document `_ 296 | -------------------------------------------------------------------------------- /bootstrap.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2006 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Bootstrap a buildout-based project 15 | 16 | Simply run this script in a directory containing a buildout.cfg. 17 | The script accepts buildout command-line options, so you can 18 | use the -c option to specify an alternate configuration file. 19 | """ 20 | 21 | import os 22 | import shutil 23 | import sys 24 | import tempfile 25 | 26 | from optparse import OptionParser 27 | 28 | tmpeggs = tempfile.mkdtemp() 29 | 30 | usage = '''\ 31 | [DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] 32 | 33 | Bootstraps a buildout-based project. 34 | 35 | Simply run this script in a directory containing a buildout.cfg, using the 36 | Python that you want bin/buildout to use. 37 | 38 | Note that by using --find-links to point to local resources, you can keep 39 | this script from going over the network. 40 | ''' 41 | 42 | parser = OptionParser(usage=usage) 43 | parser.add_option("-v", "--version", help="use a specific zc.buildout version") 44 | 45 | parser.add_option("-t", "--accept-buildout-test-releases", 46 | dest='accept_buildout_test_releases', 47 | action="store_true", default=False, 48 | help=("Normally, if you do not specify a --version, the " 49 | "bootstrap script and buildout gets the newest " 50 | "*final* versions of zc.buildout and its recipes and " 51 | "extensions for you. If you use this flag, " 52 | "bootstrap and buildout will get the newest releases " 53 | "even if they are alphas or betas.")) 54 | parser.add_option("-c", "--config-file", 55 | help=("Specify the path to the buildout configuration " 56 | "file to be used.")) 57 | parser.add_option("-f", "--find-links", 58 | help=("Specify a URL to search for buildout releases")) 59 | 60 | 61 | options, args = parser.parse_args() 62 | 63 | ###################################################################### 64 | # load/install setuptools 65 | 66 | to_reload = False 67 | try: 68 | import pkg_resources 69 | import setuptools 70 | except ImportError: 71 | ez = {} 72 | 73 | try: 74 | from urllib.request import urlopen 75 | except ImportError: 76 | from urllib2 import urlopen 77 | 78 | # XXX use a more permanent ez_setup.py URL when available. 79 | exec(urlopen('https://bitbucket.org/pypa/setuptools/raw/0.7.2/ez_setup.py' 80 | ).read(), ez) 81 | setup_args = dict(to_dir=tmpeggs, download_delay=0) 82 | ez['use_setuptools'](**setup_args) 83 | 84 | if to_reload: 85 | reload(pkg_resources) 86 | import pkg_resources 87 | # This does not (always?) update the default working set. We will 88 | # do it. 89 | for path in sys.path: 90 | if path not in pkg_resources.working_set.entries: 91 | pkg_resources.working_set.add_entry(path) 92 | 93 | ###################################################################### 94 | # Install buildout 95 | 96 | ws = pkg_resources.working_set 97 | 98 | cmd = [sys.executable, '-c', 99 | 'from setuptools.command.easy_install import main; main()', 100 | '-mZqNxd', tmpeggs] 101 | 102 | find_links = os.environ.get( 103 | 'bootstrap-testing-find-links', 104 | options.find_links or 105 | ('http://downloads.buildout.org/' 106 | if options.accept_buildout_test_releases else None) 107 | ) 108 | if find_links: 109 | cmd.extend(['-f', find_links]) 110 | 111 | setuptools_path = ws.find( 112 | pkg_resources.Requirement.parse('setuptools')).location 113 | 114 | requirement = 'zc.buildout' 115 | version = options.version 116 | if version is None and not options.accept_buildout_test_releases: 117 | # Figure out the most recent final version of zc.buildout. 118 | import setuptools.package_index 119 | _final_parts = '*final-', '*final' 120 | 121 | def _final_version(parsed_version): 122 | for part in parsed_version: 123 | if (part[:1] == '*') and (part not in _final_parts): 124 | return False 125 | return True 126 | index = setuptools.package_index.PackageIndex( 127 | search_path=[setuptools_path]) 128 | if find_links: 129 | index.add_find_links((find_links,)) 130 | req = pkg_resources.Requirement.parse(requirement) 131 | if index.obtain(req) is not None: 132 | best = [] 133 | bestv = None 134 | for dist in index[req.project_name]: 135 | distv = dist.parsed_version 136 | if _final_version(distv): 137 | if bestv is None or distv > bestv: 138 | best = [dist] 139 | bestv = distv 140 | elif distv == bestv: 141 | best.append(dist) 142 | if best: 143 | best.sort() 144 | version = best[-1].version 145 | if version: 146 | requirement = '=='.join((requirement, version)) 147 | cmd.append(requirement) 148 | 149 | import subprocess 150 | if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: 151 | raise Exception( 152 | "Failed to execute command:\n%s", 153 | repr(cmd)[1:-1]) 154 | 155 | ###################################################################### 156 | # Import and run buildout 157 | 158 | ws.add_entry(tmpeggs) 159 | ws.require(requirement) 160 | import zc.buildout.buildout 161 | 162 | if not [a for a in args if '=' not in a]: 163 | args.append('bootstrap') 164 | 165 | # if -c was provided, we push it back into args for buildout' main function 166 | if options.config_file is not None: 167 | args[0:0] = ['-c', options.config_file] 168 | 169 | zc.buildout.buildout.main(args) 170 | shutil.rmtree(tmpeggs) 171 | -------------------------------------------------------------------------------- /buildout.cfg: -------------------------------------------------------------------------------- 1 | [buildout] 2 | extensions = 3 | mr.developer 4 | parts = alipay 5 | versions = versions 6 | allow-picked-versions = true 7 | develop = . 8 | 9 | [alipay] 10 | recipe = zc.recipe.egg 11 | interpreter = python 12 | eggs = 13 | alipay 14 | bpython 15 | 16 | [versions] 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import io 4 | from setuptools import setup 5 | from setuptools import find_packages 6 | 7 | here = os.path.abspath(os.path.dirname(__file__)) 8 | README = io.open(os.path.join(here, 'README.rst'), encoding='UTF-8').read() 9 | CHANGES = io.open(os.path.join(here, 'CHANGES.rst'), encoding='UTF-8').read() 10 | 11 | setup(name='alipay', 12 | version='0.7.4', 13 | description='An Unofficial Alipay API for Python', 14 | long_description=README + '\n\n' + CHANGES, 15 | classifiers=[ 16 | 'Intended Audience :: Developers', 17 | 'License :: OSI Approved :: BSD License', 18 | 'Operating System :: OS Independent', 19 | 'Programming Language :: Python :: 2', 20 | 'Programming Language :: Python :: 2.6', 21 | 'Programming Language :: Python :: 2.7', 22 | 'Programming Language :: Python :: 3', 23 | 'Programming Language :: Python :: 3.3', 24 | 'Programming Language :: Python :: 3.4', 25 | 'Programming Language :: Python :: 3.5', 26 | 'Topic :: Software Development :: Libraries :: Python Modules', 27 | ], 28 | keywords='alipay', 29 | author='Eric Lo', 30 | author_email='lxneng@gmail.com', 31 | url='https://github.com/lxneng/alipay', 32 | license='BSD', 33 | packages=find_packages('src'), 34 | package_dir={'': 'src'}, 35 | include_package_data=True, 36 | zip_safe=False, 37 | install_requires=['requests', 'six', 'pytz'], 38 | test_suite='alipay.tests') 39 | -------------------------------------------------------------------------------- /src/alipay/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | import time 4 | from hashlib import md5 5 | from datetime import datetime 6 | from xml.etree import ElementTree 7 | from collections import OrderedDict 8 | 9 | import six 10 | import requests 11 | from pytz import timezone 12 | 13 | if six.PY3: 14 | from urllib.parse import ( 15 | parse_qs, 16 | urlparse, 17 | unquote, 18 | urlencode 19 | ) 20 | else: 21 | from six.moves.urllib.parse import ( 22 | parse_qs, 23 | urlparse, 24 | unquote, 25 | urlencode 26 | ) 27 | 28 | from .exceptions import MissingParameter 29 | from .exceptions import ParameterValueError 30 | from .exceptions import TokenAuthorizationError 31 | 32 | 33 | def encode_dict(params): 34 | return {k: six.u(v).encode('utf-8') 35 | if isinstance(v, str) else v.encode('utf-8') 36 | if isinstance(v, six.string_types) else v 37 | for k, v in six.iteritems(params)} 38 | 39 | 40 | class Alipay(object): 41 | 42 | GATEWAY_URL = 'https://mapi.alipay.com/gateway.do' 43 | 44 | NOTIFY_GATEWAY_URL = 'https://mapi.alipay.com/gateway.do'\ 45 | '?service=notify_verify&partner=%s¬ify_id=%s' 46 | 47 | sign_tuple = ('sign_type', 'MD5', 'MD5') 48 | sign_key = False 49 | 50 | def __init__(self, pid, key, seller_email=None, seller_id=None): 51 | self.key = key 52 | self.pid = pid 53 | self.default_params = {'_input_charset': 'utf-8', 54 | 'partner': pid, 55 | 'payment_type': '1'} 56 | # 优先使用 seller_id (与接口端的行为一致) 57 | if seller_id is not None: 58 | self.default_params['seller_id'] = seller_id 59 | elif seller_email is not None: 60 | self.default_params['seller_email'] = seller_email 61 | else: 62 | raise ParameterValueError( 63 | "seller_email and seller_id must have one.") 64 | 65 | def _generate_md5_sign(self, params): 66 | src = '&'.join(['%s=%s' % (key, value) for key, 67 | value in sorted(params.items())]) + self.key 68 | return md5(src.encode('utf-8')).hexdigest() 69 | 70 | def _check_params(self, params, names): 71 | if not all(k in params for k in names): 72 | raise MissingParameter('missing parameters') 73 | 74 | def _build_url(self, service, paramnames=None, **kw): 75 | ''' 76 | 创建带签名的请求地址,paramnames为需要包含的参数名,用于避免出现过多的参数,默认使用全部参数 77 | ''' 78 | params = self.default_params.copy() 79 | params['service'] = service 80 | params.update(kw) 81 | if paramnames: 82 | params = dict([(k, params[k]) for k in paramnames if k in params]) 83 | signkey, signvalue, signdescription = self.sign_tuple 84 | signmethod = getattr( 85 | self, 86 | '_generate_%s_sign' % signdescription.lower(), 87 | None # getattr raise AttributeError if not default provided 88 | ) 89 | if signmethod is None: 90 | raise NotImplementedError( 91 | "This type '%s' of sign is not implemented yet." 92 | % signdescription) 93 | if self.sign_key: 94 | params.update({signkey: signvalue}) 95 | params.update({signkey: signvalue, 96 | 'sign': signmethod(params)}) 97 | 98 | return '%s?%s' % (self.GATEWAY_URL, urlencode(encode_dict(params))) 99 | 100 | def create_direct_pay_by_user_url(self, **kw): 101 | '''即时到帐''' 102 | self._check_params(kw, ('out_trade_no', 'subject')) 103 | 104 | if not kw.get('total_fee') and \ 105 | not (kw.get('price') and kw.get('quantity')): 106 | raise ParameterValueError( 107 | 'total_fee or (price && quantiry) must have one.') 108 | 109 | url = self._build_url('create_direct_pay_by_user', **kw) 110 | return url 111 | 112 | def refund_fastpay_by_platform_pwd(self, 113 | batch_list=(), 114 | tzinfo='Asia/Shanghai', 115 | **kw): 116 | '''即时到账有密退款''' 117 | self._check_params(kw, ('batch_no')) 118 | 119 | batch_no = kw['batch_no'] 120 | detail_data = [] 121 | total_num = 0 122 | print(batch_list) 123 | for itm in batch_list: 124 | if itm == None: 125 | continue 126 | total_num += 1 127 | detail_data.append('^'.join((itm['trade_no'], 128 | str(itm['fee']), 129 | itm['note']))) 130 | 131 | kw['detail_data'] = '#'.join(detail_data) 132 | utcnow = datetime.utcnow() 133 | local_now = timezone(tzinfo).fromutc(utcnow) 134 | kw['batch_num'] = total_num 135 | kw['email'] = self.default_params['seller_email'] 136 | kw['refund_date'] = local_now.strftime('%Y-%m-%d %H:%M:%S') 137 | url = self._build_url('refund_fastpay_by_platform_pwd', **kw) 138 | return url 139 | 140 | def create_partner_trade_by_buyer_url(self, **kw): 141 | '''担保交易''' 142 | names = ('out_trade_no', 'subject', 'logistics_type', 143 | 'logistics_fee', 'logistics_payment', 'price', 'quantity') 144 | self._check_params(kw, names) 145 | url = self._build_url('create_partner_trade_by_buyer', **kw) 146 | return url 147 | 148 | def create_batch_trans_notify_url(self, 149 | batch_list=(), 150 | tzinfo='Asia/Shanghai', 151 | **kw): 152 | '''批量付款''' 153 | names = ('account_name', 'batch_no', 'notify_url') 154 | self._check_params(kw, names) 155 | batch_no = kw['batch_no'] 156 | detail_data = '' 157 | total_fee = 0.0 158 | total_num = 0 159 | for itm in batch_list: 160 | total_fee += float(itm['fee']) 161 | total_num += 1 162 | detail_data += '^'.join((batch_no + str(total_num), 163 | itm['account'], itm['name'], 164 | str(itm['fee']), itm['note'] + '|')) 165 | kw['detail_data'] = detail_data 166 | utcnow = datetime.utcnow() 167 | local_now = timezone(tzinfo).fromutc(utcnow) 168 | kw['batch_num'] = total_num 169 | kw['batch_fee'] = total_fee 170 | kw['email'] = self.default_params['seller_email'] 171 | kw['pay_date'] = local_now.strftime('%Y%m%d') 172 | url = self._build_url('batch_trans_notify', **kw) 173 | return url 174 | 175 | def trade_create_by_buyer_url(self, **kw): 176 | '''标准双接口''' 177 | names = ('out_trade_no', 'subject', 'logistics_type', 178 | 'logistics_fee', 'logistics_payment', 'price', 'quantity') 179 | self._check_params(kw, names) 180 | 181 | url = self._build_url('trade_create_by_buyer', **kw) 182 | return url 183 | 184 | def create_forex_trade_url(self, **kw): 185 | '''Create website payment for foreigners (With QR code)''' 186 | names = ('out_trade_no', 'subject') 187 | self._check_params(kw, names) 188 | 189 | url = self._build_url('create_forex_trade', **kw) 190 | return url 191 | 192 | def create_forex_trade_wap_url(self, **kw): 193 | '''Create mobile payment for foreigners''' 194 | names = ('out_trade_no', 'subject') 195 | self._check_params(kw, names) 196 | 197 | url = self._build_url('create_forex_trade_wap', **kw) 198 | return url 199 | 200 | def add_alipay_qrcode_url(self, **kw): 201 | '''二维码管理 - 添加''' 202 | self._check_params(kw, ('biz_data', 'biz_type')) 203 | 204 | utcnow = datetime.utcnow() 205 | shanghainow = timezone('Asia/Shanghai').fromutc(utcnow) 206 | 207 | kw['method'] = 'add' 208 | kw['timestamp'] = shanghainow.strftime('%Y-%m-%d %H:%M:%S') 209 | 210 | url = self._build_url('alipay.mobile.qrcode.manage', **kw) 211 | return url 212 | 213 | def send_goods_confirm_by_platform(self, **kw): 214 | ''''确认发货''' 215 | names = ('trade_no', 'logistics_name') 216 | self._check_params(kw, names) 217 | url = self._build_url('send_goods_confirm_by_platform', **kw) 218 | return url 219 | 220 | def add_alipay_qrcode(self, **kw): 221 | return requests.get(self.add_alipay_qrcode_url(**kw)) 222 | 223 | def get_sign_method(self, **kw): 224 | signkey, signvalue, signdescription = self.sign_tuple 225 | signmethod = getattr( 226 | self, 227 | '_generate_%s_sign' % signdescription.lower(), 228 | None 229 | ) 230 | if signmethod is None: 231 | raise NotImplementedError( 232 | "This type '%s' of sign is not implemented yet." 233 | % signdescription) 234 | return signmethod 235 | 236 | def verify_notify(self, **kw): 237 | sign = kw.pop('sign', '') 238 | kw.pop('sign_type', '') 239 | signmethod = self.get_sign_method(**kw) 240 | if signmethod(kw) == sign: 241 | return self.check_notify_remotely(**kw) 242 | else: 243 | return False 244 | 245 | def check_notify_remotely(self, **kw): 246 | remote_result = requests.get( 247 | self.NOTIFY_GATEWAY_URL % (self.pid, kw['notify_id']), 248 | headers={'connection': 'close'} 249 | ).text 250 | return remote_result == 'true' 251 | 252 | def single_trade_query(self, **kw): 253 | ''' 254 | 单笔交易查询,返回xml. 255 | out_trade_no或者trade_no参数必须有一个. 256 | 该接口需要联系支付宝客服签约. 257 | ''' 258 | if 'trade_no' not in kw and 'out_trade_no' not in kw: 259 | raise MissingParameter('missing parameters') 260 | url = self._build_url('single_trade_query', paramnames=['service', 'partner', '_input_charset', 'sign', 'sign_type', 'trade_no', 'out_trade_no'], **kw) 261 | remote_result = requests.get(url, headers={'connection': 'close'}).text 262 | return remote_result 263 | 264 | '''Wap支付接口''' 265 | 266 | 267 | class WapAlipay(Alipay): 268 | GATEWAY_URL = 'http://wappaygw.alipay.com/service/rest.htm' 269 | TOKEN_ROOT_NODE = 'direct_trade_create_req' 270 | AUTH_ROOT_NODE = 'auth_and_execute_req' 271 | _xmlnode = '<%s>%s' 272 | sign_tuple = ('sec_id', 'MD5', 'MD5') 273 | sign_key = True 274 | 275 | def __init__(self, pid, key, seller_email): 276 | super(WapAlipay, self).__init__(pid, key, seller_email) 277 | self.seller_email = seller_email 278 | self.default_params = {'format': 'xml', 279 | 'v': '2.0', 280 | 'partner': pid, 281 | '_input_charset': 'utf-8', 282 | } 283 | 284 | def create_direct_pay_token_url(self, **kw): 285 | '''即时到帐token''' 286 | names = ('subject', 'out_trade_no', 'total_fee', 'seller_account_name', 287 | 'call_back_url', ) 288 | self._check_params(kw, names) 289 | req_data = ''.join([self._xmlnode % (key, value, key) 290 | for (key, value) in six.iteritems(kw)]) 291 | req_data = self._xmlnode % ( 292 | self.TOKEN_ROOT_NODE, req_data, self.TOKEN_ROOT_NODE) 293 | if '&' in req_data: 294 | raise ParameterValueError('character \'&\' is not allowed.') 295 | params = {'req_data': req_data, 'req_id': time.time()} 296 | url = self._build_url('alipay.wap.trade.create.direct', **params) 297 | return url 298 | 299 | def create_direct_pay_by_user_url(self, **kw): 300 | '''即时到帐''' 301 | if 'token' not in kw: 302 | url = self.create_direct_pay_token_url(**kw) 303 | alipayres = requests.post( 304 | url, headers={'connection': 'close'}).text 305 | params = parse_qs(urlparse(alipayres).path, keep_blank_values=True) 306 | if 'res_data' in params: 307 | tree = ElementTree.ElementTree( 308 | ElementTree.fromstring(unquote(params['res_data'][0]))) 309 | token = tree.find("request_token").text 310 | else: 311 | raise TokenAuthorizationError(unquote(params['res_error'][0])) 312 | else: 313 | token = kw['token'] 314 | params = {'req_data': self._xmlnode % 315 | (self.AUTH_ROOT_NODE, 316 | (self._xmlnode % ('request_token', token, 'request_token')), 317 | self.AUTH_ROOT_NODE)} 318 | url = self._build_url('alipay.wap.auth.authAndExecute', **params) 319 | return url 320 | 321 | def trade_create_by_buyer_url(self, **kw): 322 | raise NotImplementedError("This type of pay is not supported in wap.") 323 | 324 | def create_partner_trade_by_buyer_url(self, **kw): 325 | raise NotImplementedError("This type of pay is not supported in wap.") 326 | 327 | def check_notify_remotely(self, **kw): 328 | if 'notify_data' in kw: 329 | notifydata = unquote(kw['notify_data']) 330 | if isinstance(notifydata, str): 331 | notifydata = six.u(notifydata).encode('utf-8') 332 | elif isinstance(notifydata, six.string_types): 333 | notifydata = notifydata.encode('utf-8') 334 | tree = ElementTree.ElementTree(ElementTree.fromstring(notifydata)) 335 | return super(WapAlipay, self).check_notify_remotely( 336 | **{'notify_id': tree.find("notify_id").text}) 337 | return True 338 | 339 | def _generate_md5_notify_sign(self, kw): 340 | newpara = OrderedDict() 341 | newpara['service'] = kw['service'] 342 | newpara['v'] = kw['v'] 343 | newpara['sec_id'] = kw['sec_id'] 344 | newpara['notify_data'] = kw['notify_data'] 345 | src = '&'.join(['%s=%s' % (key, value) 346 | for key, value in newpara.items()]) + self.key 347 | return md5(src.encode('utf-8')).hexdigest() 348 | 349 | def get_sign_method(self, **kw): 350 | if 'notify_data' in kw: 351 | signkey, signvalue, signdescription = self.sign_tuple 352 | signmethod = getattr( 353 | self, 354 | '_generate_%s_notify_sign' % signdescription.lower(), 355 | None 356 | ) 357 | if signmethod is None: 358 | raise NotImplementedError( 359 | "This type '%s' of sign is not implemented yet." 360 | % signdescription) 361 | return signmethod 362 | return super(WapAlipay, self).get_sign_method(**kw) 363 | 364 | 365 | def includeme(config): 366 | settings = config.registry.settings 367 | config.registry['alipay'] = Alipay( 368 | pid=settings.get('alipay.pid'), 369 | key=settings.get('alipay.key'), 370 | seller_email=settings.get('alipay.seller_email')) 371 | -------------------------------------------------------------------------------- /src/alipay/exceptions.py: -------------------------------------------------------------------------------- 1 | class AlipayException(Exception): 2 | '''Base Alipay Exception''' 3 | 4 | 5 | class MissingParameter(AlipayException): 6 | """Raised when the create payment url process is missing some 7 | parameters needed to continue""" 8 | 9 | 10 | class ParameterValueError(AlipayException): 11 | """Raised when parameter value is incorrect""" 12 | 13 | 14 | class TokenAuthorizationError(AlipayException): 15 | '''The error occurred when getting token ''' 16 | -------------------------------------------------------------------------------- /src/alipay/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import six 4 | from xml.etree import ElementTree 5 | if six.PY3: 6 | from urllib.parse import parse_qs, urlparse 7 | else: 8 | from urlparse import parse_qs, urlparse 9 | 10 | 11 | class AlipayTests(unittest.TestCase): 12 | 13 | def Alipay(self, *a, **kw): 14 | from alipay import Alipay 15 | return Alipay(*a, **kw) 16 | 17 | def WapAlipay(self, *a, **kw): 18 | from alipay import WapAlipay 19 | return WapAlipay(*a, **kw) 20 | 21 | def setUp(self): 22 | self.alipay = self.Alipay(pid='pid', key='key', 23 | seller_email='lxneng@gmail.com') 24 | self.wapalipay = self.WapAlipay(pid='pid', key='key', 25 | seller_email='lxneng@gmail.com') 26 | 27 | def test_create_direct_pay_by_user_url(self): 28 | params = {'out_trade_no': '1', 29 | 'subject': 'test', 30 | 'price': '0.01', 31 | 'quantity': 1} 32 | self.assertIn('create_direct_pay_by_user', 33 | self.alipay.create_direct_pay_by_user_url(**params)) 34 | 35 | def test_create_direct_pay_by_user_url_with_unicode(self): 36 | params = {'out_trade_no': '1', 37 | 'subject': u'测试', 38 | 'price': '0.01', 39 | 'quantity': 1} 40 | self.assertIn('create_direct_pay_by_user', 41 | self.alipay.create_direct_pay_by_user_url(**params)) 42 | 43 | def test_create_partner_trade_by_buyer_url(self): 44 | params = {'out_trade_no': '1', 45 | 'subject': 'test', 46 | 'logistics_type': 'POST', 47 | 'logistics_fee': '0', 48 | 'logistics_payment': 'SELLER_PAY', 49 | 'price': '0.01', 50 | 'quantity': 1} 51 | self.assertIn('create_partner_trade_by_buyer', 52 | self.alipay.create_partner_trade_by_buyer_url(**params)) 53 | 54 | def test_create_batch_trans_notify_url(self): 55 | batch_list = ({'account': 'zjqq930112@sina.com', 56 | 'name': u'姓名', 57 | 'fee': '0.01', 58 | 'note': 'test'}, 59 | {'account': 'zjqq930112@sina.com', 60 | 'name': u'姓名', 61 | 'fee': '0.01', 62 | 'note': 'test'}) 63 | params = {'batch_list': batch_list, 64 | 'account_name': 'test_name', 65 | 'batch_no': 'test_no', 66 | 'notify_url': 'www.test.com'} 67 | self.assertIn('batch_trans_notify', 68 | self.alipay.create_batch_trans_notify_url(**params)) 69 | 70 | def test_trade_create_by_buyer_url(self): 71 | params = {'out_trade_no': '1', 72 | 'subject': 'test', 73 | 'logistics_type': 'POST', 74 | 'logistics_fee': '0', 75 | 'logistics_payment': 'SELLER_PAY', 76 | 'price': '0.01', 77 | 'quantity': 1} 78 | self.assertIn('trade_create_by_buyer', 79 | self.alipay.trade_create_by_buyer_url(**params)) 80 | 81 | def test_create_forex_trade_url(self): 82 | params = {'out_trade_no': '1', 83 | 'subject': 'test', 84 | 'logistics_type': 'POST', 85 | 'logistics_fee': '0', 86 | 'logistics_payment': 'SELLER_PAY', 87 | 'price': '0.01', 88 | 'quantity': 1} 89 | self.assertIn('create_forex_trade', 90 | self.alipay.create_forex_trade_url(**params)) 91 | 92 | def test_create_forex_trade_wap_url(self): 93 | params = {'out_trade_no': '1', 94 | 'subject': 'test', 95 | 'logistics_type': 'POST', 96 | 'logistics_fee': '0', 97 | 'logistics_payment': 'SELLER_PAY', 98 | 'price': '0.01', 99 | 'quantity': 1} 100 | self.assertIn('create_forex_trade_wap', 101 | self.alipay.create_forex_trade_wap_url(**params)) 102 | 103 | def test_send_goods_confirm_by_platform(self): 104 | params = { 105 | 'trade_no': 1, 106 | 'logistics_name': 'XXXX', 107 | 'transport_type': 'EXPRESS', 108 | 'invoice_no': 'AAAAA' 109 | } 110 | self.assertIn('send_goods_confirm_by_platform', 111 | self.alipay.send_goods_confirm_by_platform(**params)) 112 | 113 | def test_add_alipay_qrcode(self): 114 | import json 115 | params = { 116 | 'biz_data': json.dumps({ 117 | 'goods_info': { 118 | 'id': '123456', 119 | 'name': u'测试', 120 | 'price': '0.01' 121 | }, 122 | 'need_address': 'F', 123 | 'trade_type': '1' 124 | }, ensure_ascii=False), 125 | 'biz_type': '10' 126 | } 127 | self.assertIn('alipay.mobile.qrcode.manage', 128 | self.alipay.add_alipay_qrcode_url(**params)) 129 | 130 | def test_raise_missing_parameter_in_create_direct_pay_by_user_url(self): 131 | from .exceptions import MissingParameter 132 | params = {'out_trade_no': '1', 133 | 'price': '0.01', 134 | 'quantity': 1} 135 | self.assertRaises(MissingParameter, 136 | self.alipay.create_direct_pay_by_user_url, **params) 137 | 138 | def test_raise_parameter_value_error_in_create_direct_pay_by_user_url(self 139 | ): 140 | from .exceptions import ParameterValueError 141 | params = {'out_trade_no': '1', 142 | 'subject': 'test', 143 | 'quantity': 1} 144 | self.assertRaises(ParameterValueError, 145 | self.alipay.create_direct_pay_by_user_url, 146 | **params) 147 | 148 | def test_raise_parameter_value_error_when_initializing(self): 149 | from .exceptions import ParameterValueError 150 | self.assertRaises(ParameterValueError, 151 | self.Alipay, pid='pid', key='key') 152 | 153 | def test_create_wap_direct_pay_by_user_url(self): 154 | params = {'out_trade_no': '1', 155 | 'subject': u'测试', 156 | 'total_fee': '0.01', 157 | 'seller_account_name': self.wapalipay.seller_email, 158 | 'call_back_url': 'http://mydomain.com/alipay/callback'} 159 | url = self.wapalipay.create_direct_pay_token_url(**params) 160 | self.assertIn('alipay.wap.trade.create.direct', url) 161 | params = parse_qs(urlparse(url).query, keep_blank_values=True) 162 | self.assertIn('req_data', params) 163 | self.assertIn('sec_id', params) 164 | tree = ElementTree.ElementTree( 165 | ElementTree.fromstring(params['req_data'][0])) 166 | self.assertEqual(self.wapalipay.TOKEN_ROOT_NODE, tree.getroot().tag) 167 | 168 | def test_wap_notimplemented_pay(self): 169 | params = {} 170 | self.assertRaises(NotImplementedError, 171 | self.wapalipay.trade_create_by_buyer_url, **params) 172 | self.assertRaises(NotImplementedError, 173 | self.wapalipay.create_partner_trade_by_buyer_url, 174 | **params) 175 | 176 | def test_wap_unauthorization_token(self): 177 | from .exceptions import TokenAuthorizationError 178 | params = {'out_trade_no': '1', 179 | 'subject': u'测试', 180 | 'total_fee': '0.01', 181 | 'seller_account_name': self.wapalipay.seller_email, 182 | 'call_back_url': 'http://mydomain.com/alipay/callback'} 183 | self.assertRaises(TokenAuthorizationError, 184 | self.wapalipay.create_direct_pay_by_user_url, 185 | **params) 186 | 187 | def test_wap_notifyurl(self): 188 | '''valid MD5 sign 189 | invalid notify id but should not throw any exception 190 | 191 | sec_id=MD5&v=1.0¬ify_data=1测试2014080239826696xxx@gmail.com2014-08-02 14:49:13trade_status_sync1BD8Y9JQ2LT8MVXLMT34RTUWEMMBAXMIGBVQGF5CQNZHPYPQHSD4MEI56NQD2OLNV2014-08-02 15:14:252088411445328172TRADE_FINISHEDN0.032014-08-02 14:49:27lxneng@gmail.com2014-08-02 14:49:270.0320880022930779676a40ac71fcf17d99b5274b0c6c8970ea7cN&service=alipay.wap.trade.create.direct&sign=1f0a524dc51ed5bfc7ee2bac62e39534 192 | ''' 193 | params = {'sec_id': 'MD5', 'v': '1.0', 194 | 'notify_data': u'1测试2014080239826696xxx@gmail.com2014-08-02 14:49:13trade_status_sync1BD8Y9JQ2LT8MVXLMT34RTUWEMMBAXMIGBVQGF5CQNZHPYPQHSD4MEI56NQD2OLNV2014-08-02 15:14:252088411445328172TRADE_FINISHEDN0.032014-08-02 14:49:27lxneng@gmail.com2014-08-02 14:49:270.0320880022930779676a40ac71fcf17d99b5274b0c6c8970ea7cN', 195 | 'service': 'alipay.wap.trade.create.direct', 196 | 'sign': '1f0a524dc51ed5bfc7ee2bac62e39534'} 197 | rt = self.wapalipay.verify_notify(**params) 198 | self.assertFalse(rt) 199 | 200 | def test_single_trade_query(self): 201 | ''' single_trade_query will response a ILLEGAL_PARTNER error xml document, like: 202 | 203 | FILLEGAL_PARTNER 204 | ''' 205 | self.assertIn('ILLEGAL_PARTNER', self.alipay.single_trade_query(out_trade_no='2015102012')) -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34 3 | 4 | [testenv] 5 | commands = python setup.py test 6 | --------------------------------------------------------------------------------