今天是 π 节,祝大家 π 节快乐 :D
通常大家使用 Python 提供的包的时候,无外乎使用 pip
完成安装,然后在源代码头部使用 import
引入需要的包,再来就可以愉快地使用了。对于初学者来说,可能会困惑,要如何构建这样一个简单易用的包呢?
由此本文应运而生,这里将带大家一步一步实现一个这样的 Python 包。包的功能是封装钉钉群聊中自定义机器人的 HTTP API,方便 Python 用户直接调用该包实现机器人的消息发送。点击此处可以查看钉钉自定义机器人的文档。
整个过程包含以下几步:
根据钉钉自定义机器人的文档,可以通过向如下的链接发送 POST 请求来实现发送消息的功能。
https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxx
链接中的 xxxxxxxx
为 Token
,该值可以在机器人的配置中找到。
发送文本消息需要 POST 的内容如下,发送其他消息类型可以参看文档。
{
"msgtype": "text",
"text": {
"content": "我就是我, 是不一样的烟火"
}
}
发送 POST 请求的操作可以使用 Python 中的 requests
库来实现,下面就可以写代码了。
这里将该库命名为 dingtalk_robot
。找一个空文件夹,新建一个 dingtalk_robot.py
文件。这里确定仅需要一个源代码文件,如果有多个源代码文件,可以建立一个名为 dingtalk_robot
的文件夹,并在该文件夹中使用 __init__.py
引入各个源代码文件。
dingtalk_robot.py
中,实现钉钉自定义机器人的 HTTP API:
import requests
class DingtalkRobot:
BaseUrl = 'https://oapi.dingtalk.com/robot/send?access_token='
def __init__(self, token: str):
self.token = token
self.access_url = self.BaseUrl + token
def send_text(self, content: str):
message = {
'msgtype': 'text',
'text': {
'content': content
}
}
response = requests.post(url=self.access_url, json=message)
status = response.json()
if status['errcode'] != 0:
raise DingtalkRobot.Error('Error Code: {}, {}'.format(
status['errcode'],
status['errmsg']
))
class Error(ValueError):
pass
注意到代码中自定义了一个 Error
类。钉钉自定义机器人的 HTTP 接口有状态返回码,如果返回值异常则抛出这样一个 Error
。
另外需要注意 requests
可能也会抛出网络相关的异常类,使用包的过程中同样需要做好异常处理。
至此本步骤完成。
单元测试,是针对程序模块来进行正确性检验的测试工作。Python 自带了单元测试框架 unittest
,点击此处可以查看 unittest
的官方文档。
单元测试简单来说,就是在正常使用一个类或者一个函数的功能,并在测试中对比预期结果与运行结果是否一致。如果不一致则说明代码出了问题。
在同上的文件夹中,新建单元测试文件 test_dingtalk_robot.py
。按照 unittest
的文档,编写如下的测试代码:
import unittest
from dingtalk_robot import DingtalkRobot
class MyTestCase(unittest.TestCase):
valid_token = 'e2bfbac46ed921563dcd852ae65b3adc7797db997ea6c2cc75843b74e4365842'
invalid_token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
def test_send_text(self):
dingtalk_robot = DingtalkRobot(token=self.valid_token)
success = dingtalk_robot.send_text('Send text to Dingding')
self.assertTrue(success)
def test_send_text_to_invalid_token(self):
dingtalk_robot = DingtalkRobot(token=self.invalid_token)
error_here = False
try:
dingtalk_robot.send_text('Send text to Dingding')
except dingtalk_robot.Error as e:
error_here = True
self.assertTrue(error_here)
if __name__ == '__main__':
unittest.main()
这里分别测试了向正常 token
和异常 token
发送消息。正常 token
发送成功,而异常 token
发送失败抛出异常。
测试代码完成后,可以运行单元测试命令进行测试。这里推荐使用 pytest
。使用 pip
安装,然后执行 pytest
,得到如下的输出:
==================== test session starts =====================
platform darwin -- Python 3.6.4, pytest-2.9.2, py-1.4.33, pluggy-0.3.1
benchmark: 3.1.1 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /Users/sfzhou/Code/07_Python/04_DingtalkRobot, inifile:
plugins: tornado-0.4.5, timeout-1.2.1, pep8-1.0.6, cov-2.5.1, benchmark-3.1.1
collected 2 items
test_dingtalk_robot.py ..
================== 2 passed in 0.52 seconds ==================
可以看到两个测试均通过,本步骤完成。
接下来可以打包上述的 dingtalk_robot
工具了。Python 的打包还是比较简单的,编写一个 setup.py
,写入相关信息即可。点击此处可以查看打包的详细文档。
按照文档,继续新建一个 setup.py
文件,写入如下代码:
"""
Python Package for Dingtalk Robot
Author: SF-Zhou
Date: 2018-03-15
"""
import dingtalk_robot
from setuptools import setup
setup(
name=dingtalk_robot.__name__,
version=dingtalk_robot.__version__,
description=dingtalk_robot.__description__,
url=dingtalk_robot.__github__,
author=dingtalk_robot.__author__,
author_email=dingtalk_robot.__email__,
license='MIT',
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3',
],
keywords='tools dingtalk robot',
py_modules=['dingtalk_robot'],
install_requires=['requests'],
extras_require={'test': ['pytest']}
)
可以看到,这里引入了一个 setup
函数,并且向该函数传递了很多关于 dingtalk_robot
的信息。上述的 dingtalk_robot
的信息还需要在 dingtalk_robot.py
中补充,在 dingtalk_robot.py
文件头部可以继续加上:
__version__ = '0.0.1'
__author__ = 'SF-Zhou'
__email__ = 'sfzhou.scut@gmail.com'
__github__ = 'https://github.com/SF-Zhou/DingtalkRobot'
__description__ = 'Python Package for Dingtalk Robot'
install_requires
参数中描述了该包的依赖,dingtalk_robot
的外部依赖只有 requests
。对于一个 Python 项目通常也会使用 requirements.txt
来描述依赖,故而新建一个 requirements.txt
,文件内容仅填入一行 requests
即可。当其他人需要手动安装依赖的时候,可以执行:
pip3 install -r requirements.txt
各项配置工作均已完成,下面可以执行 build
命令构建包:
python setup.py build
可以看到类似如下的输出:
running build
running build_py
creating build
creating build/lib
copying dingtalk_robot.py -> build/lib
warning: build_py: byte-compiling is disabled, skipping.
如果希望把该包安装到本地,可以执行:
python setup.py install
这样在本地的环境中,可以直接通过 import dingtalk_robot
来使用该包了。
如果希望可以将该包发布出去,被大家广泛地使用,那还需要构建一个源码包:
python setup.py sdist
可以看到类似如下的输出:
running sdist
running egg_info
creating dingtalk_robot.egg-info
writing dingtalk_robot.egg-info/PKG-INFO
writing dependency_links to dingtalk_robot.egg-info/dependency_links.txt
writing requirements to dingtalk_robot.egg-info/requires.txt
writing top-level names to dingtalk_robot.egg-info/top_level.txt
writing manifest file 'dingtalk_robot.egg-info/SOURCES.txt'
reading manifest file 'dingtalk_robot.egg-info/SOURCES.txt'
writing manifest file 'dingtalk_robot.egg-info/SOURCES.txt'
running check
creating dingtalk_robot-0.0.1
creating dingtalk_robot-0.0.1/dingtalk_robot.egg-info
copying files to dingtalk_robot-0.0.1...
copying README.md -> dingtalk_robot-0.0.1
copying dingtalk_robot.py -> dingtalk_robot-0.0.1
copying setup.py -> dingtalk_robot-0.0.1
copying dingtalk_robot.egg-info/PKG-INFO -> dingtalk_robot-0.0.1/dingtalk_robot.egg-info
copying dingtalk_robot.egg-info/SOURCES.txt -> dingtalk_robot-0.0.1/dingtalk_robot.egg-info
copying dingtalk_robot.egg-info/dependency_links.txt -> dingtalk_robot-0.0.1/dingtalk_robot.egg-info
copying dingtalk_robot.egg-info/requires.txt -> dingtalk_robot-0.0.1/dingtalk_robot.egg-info
copying dingtalk_robot.egg-info/top_level.txt -> dingtalk_robot-0.0.1/dingtalk_robot.egg-info
Writing dingtalk_robot-0.0.1/setup.cfg
creating dist
Creating tar archive
removing 'dingtalk_robot-0.0.1' (and everything under it)
并且当前文件夹下会多出两个新文件夹:dist
和 dingtalk_robot.egg-info
。dist
文件夹中有一个名为 dingtalk_robot-0.0.1.tar.gz
的压缩包,该包的内容为如下:
可以看到这里包含了源代码文件,还有一些 INFO 文件。将该文件发布出去,其他人也可以轻松地安装并使用 dingtalk_robot
了。
至此本步骤完成。
Python Package Index,简称 PyPI,为 Python 官方的软件库。可以在 PyPI 中查看、下载所有公开的 Python 包,也可以发布自己的 Python 包供大家使用。当然,首先需要注册一个账号,可以点击此处注册。
拥有账号后,就可以使用 twine
上传上一步骤中构建的 Python 包了。
使用 pip
安装 twine
后,需要简单配置一下。在 HOME 目录下新建一个 .pypirc
文件,即 ~/.pypirc
,填入以下信息:
[distutils]
index-servers=pypi
[pypi]
username = sfzhou
password = xxxxxxxx
上方的 username
和 password
替换为 PyPI 中的用户名和密码。然后在包所在的目录执行:
twine upload dist/dingtalk_robot-0.0.1.tar.gz
上传上一步骤中构建的 dingtalk_robot
包,可以看到类似如下的输出:
Uploading distributions to https://upload.pypi.org/legacy/
Uploading dingtalk_robot-0.0.1.tar.gz
100%|████████████████████████████████████████████| 4.57k/4.57k [00:01<00:00, 4.09kB/s]
如果显示有其他错误,则按照错误提示修正即可。比如笔者就遇到了注册邮箱没有验证的错误,完整邮箱验证后上传成功。
上传成功后,就可以在 PyPI 页面看到 dingtalk_robot
的信息了。也可以直接通过 pip
命令实现安装:
pip install dingtalk_robot
进而每个人都可以方便地安装和使用这个小工具了。
Continuous Improvement,简称 CI,也就是常说的持续集成。该步骤并不是必须的,但是配置有持续集成的项目更容易得到大家的信赖:持续集成意味着项目提供了准确的环境配置,并且通过了多项测试。
目前 GitHub 上常用的 CI 平台有 Travis-CI、AppVeyor 及 Circle-CI。本文以 Travis-CI 为例介绍 CI 的配置,点击此处可以查看 Travis-CI 的详细文档。
在项目文件夹中新建 Travis-CI 的配置文件 .travis.yml
,文件内容为:
language: python
python:
- "3.4"
- "3.5"
- "3.6"
install:
- pip install -r requirements.txt
- pip install pytest pycodestyle
script:
- pycodestyle . --max-line-length=120
- pytest
该文件首先定义了持续集成的环境为 Python,包含 3.4,3.5,3.6 三个版本。而后定义了 install
项,使用 pip
安装了运行和测试所需要的依赖。最后执行了代码风格测试和单元测试。
假设项目的文件夹已经建立好了 Git 库并上传到了 GitHub 上。例如本文中的 dingtalk_robot
,就放在 https://github.com/SF-Zhou/DingtalkRobot 中。这里还需要在 Travis-CI 中加入该项目,即在加入项目的页面打钩即可:
至此配置完成,之后每次 commit 或 pull request 后,Travis-CI 会自动执行配置中的脚本,完成代码风格测试和单元测试。
不幸的是这里没有通过测试:
可以点击失败的 Job 查看详情。原来是是因为 setup.py
文件末尾没有空行,导致代码风格检查失败:
$ pycodestyle . --max-line-length=120
./setup.py:31:2: W292 no newline at end of file
The command "pycodestyle . --max-line-length=120" exited with 1.
在 setup.py
文件末加入空行,再次提交,测试通过:
通常 CI 平台会提供项目状态的标志,点击上图上方中的 build|unknown
图标,可以得到状态标志的链接:
将该链接加入 README.md
中,之后在 GitHub 的项目页面就可以看到绿色的 Build Passing 标志了。
至此本步骤完成。
Continuous Deployment,简称 CD,持续部署。该步骤同样不是必须的,简单来说就是省事而已。
当 dingtalk_robot
通过单元测试后,下一步肯定是及时地发布到 PyPI 上。CD 的目的就是为我们自动化地完成这件事。Travis-CI 提供了非常方便的 PyPI 发布配置,点击此处可以查看相关文档。
在 .travis.yml
底部继续加入:
deploy:
provider: pypi
user: "sfzhou"
on:
tags: true
python: 3.6
当然发布还需要密码,而这里并不能直接把密码放到 GitHub 中。所以 Travis-CI 提供了一个加密密码的工具。使用 pip
安装名为 travis
的小工具,在项目目录下执行:
travis encrypt --add deploy.password
按照提示输入密码,成功后会自动把加密的密码写入 .travis.yml
中。最后将变更提交。
注意,这里配置了当且仅当运行环境为 Python 3.6,且附带 tag,才会执行发布的操作。之后需要发布的话,可以使用类似如下的命令:
# change __version__ to 0.0.5 in dingtalk_robot.py
git checkout master
git tag V0.0.5
git push origin master --tag
至此本步骤完成。
一个完整的 Python 包构建过程就结束了。麻雀虽小五脏俱全,大项目也是这样一步一步构建出来的。随着后续的继续迭代,单元测试和 CI 持续保证质量,CD 及时将包发布到 PyPI 上,GitHub 中接收社区的反馈和贡献,这个包也就会越来越完善,越来越稳定。
本文中的源代码可以在 SF-Zhou/DingtalkRobot
中找到,其中的每一个 commit 对应文中的每一步。