├── .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%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 |
--------------------------------------------------------------------------------