制作和使用三方包
制作三方包
PyODPS 自 0.11.3 起提供了 pyodps-pack
命令行工具,用于制作符合 PyODPS 及 DataWorks PyODPS
节点标准的三方包,使用方法类似 pip
命令。你可以使用该工具将所有依赖项目制作成一个 .tar.gz
压缩包,其中包含所有依照 MaxCompute / DataWorks 环境编译并打包的项目依赖。如果你的项目有自行创建的 Python
包,也可以使用该工具进行打包。
准备工作
Docker 模式
你需要安装 Docker 以顺利使用 Docker 模式运行 pyodps-pack
。 pyodps-pack
会自动调用已安装的
Docker,你不需要手动在 Docker 中运行 pyodps-pack
。对于 Linux 环境,可以参考 Docker 官方文档 安装 Docker。对于 MacOS / Windows,个人开发者可以使用
Docker Desktop 。对于没有购买过授权的企业用户,推荐使用开源的
Rancher Desktop (
中国内地镜像
)。你也可以考虑使用 minikube,
但需要一些额外的步骤,见 这份文档。我们没有在其他 Docker 环境中测试 pyodps-pack
,不保证在这些环境中的可用性。
对于期望在版本较老的专有云中的 MaxCompute / DataWorks 使用 --legacy-image
选项打包的用户,在 Windows / MacOS
或者部分内核的 Linux 系统中可能出现无法打包的错误,请参考
本文
配置合适的打包环境。
对于 Windows 用户,可能你的 Docker 服务需要依赖 Windows 系统的 Server 服务才能启动,而 Server 服务由于安全问题在很多企业被禁止启动。
在遇到问题时,请改用 Linux 打包或者设法启用 Server 服务。Rancher Desktop 在 Windows 10 下可能无法使用 containerd
作为容器引擎,可以尝试改用 dockerd
,具体参考 该文档
进行配置。
如果你的 MaxCompute / DataWorks 基于 ARM64 机型部署(通常是专有云),你需要额外增加 --arch aarch64
参数指定打包需要的架构。通常
Docker Desktop / Rancher Desktop 已经安装了跨平台打包所需的 binfmt
相关组件,你也可以使用命令
docker run --privileged --rm tonistiigi/binfmt --install arm64
安装相关的虚拟环境。该命令要求 Linux Kernel 版本高于 4.8,具体可以参考 该页面。
无 Docker 模式
备注
我们建议在打包时,尽量使用 Docker 模式。非 Docker 模式仅用于 Docker 不可用的场景,且生成的包有可能不可用。
如果你安装 Docker 遇到困难,你可以尝试使用非 Docker 模式。使用方式为新增一个 --no-docker
参数。该模式需要你的 Python
环境中已经安装 pip。如果使用该模式出现错误,请改用 Docker 模式。Windows 用户需要安装 Git bash 以使用该模式,Git bash
包含在 Git for Windows 中。
打包所有依赖
备注
MaxCompute 建议除非不得已,新项目请尽量使用 Python 3。我们不保证下面的打包步骤对 Python 2 的可用性。 旧项目在可能的情况下请尽量迁移到 Python 3 以减少后续维护的难度。
在 Linux 中使用下列命令时,请使用 sudo
调用 pyodps-pack
以保证 Docker 正常运行。
在 macOS 中使用下列命令时,不建议使用 sudo
,这可能会导致无法预期的权限问题。
安装完 PyODPS 后,你可以使用下面的命令为 Python 3 打包 pandas 及所有 pandas 的依赖项:
pyodps-pack pandas
使用非 Docker 模式打包,可以用
pyodps-pack --no-docker pandas
需要指定版本时,可以使用
pyodps-pack pandas==1.2.5
经过一系列的打包步骤,工具会显示包中的所有依赖版本
Package Version
--------------- -------
numpy 1.21.6
pandas 1.2.5
python-dateutil 2.8.2
pytz 2022.6
six 1.16.0
并在当前目录中生成一个 packages.tar.gz
文件,其中包括上面列出的所有依赖项目。
如果你希望为 Python 2.7 打包,请确定你的包要在 MaxCompute 还是 DataWorks 中使用。如果你不确定你的包将在哪个环境中使用, 请参考 这篇文章 。 如果要在 MaxCompute 中使用 Python 2.7 包,可以使用下面的打包命令:
pyodps-pack --mcpy27 pandas
如果生成的 Python 2.7 包要在 DataWorks 中使用,可以使用下面的打包命令:
pyodps-pack --dwpy27 pandas
打包自定义代码
pyodps-pack
支持打包使用 setup.py
或者 pyproject.toml
组织的用户自定义 Python project。如果你之前从未
接触过相关知识,可以参考 这个链接 获取更多信息。
下面用基于 pyproject.toml
组织的项目举例介绍一下如何使用 pyodps-pack
打包。假定项目的目录结构如下:
test_package_root
├── test_package
│ ├── __init__.py
│ ├── mod1.py
│ └── subpackage
│ ├── __init__.py
│ └── mod2.py
└── pyproject.toml
其中 pyproject.toml
内容可能为
[project]
name = "test_package"
description = "pyodps-pack example package"
version = "0.1.0"
dependencies = [
"pandas>=1.0.5"
]
完成包的开发后,使用下面的命令可以将此包和所有依赖打包进 packages.tar.gz
文件中( path_to_package
为 test_package_root
的上级路径):
pyodps-pack /<path_to_package>/test_package_root
打包 Git Repo 中的代码
pyodps-pack
支持打包远程 Git 代码仓库(例如 Github)中的代码。以 PyODPS 本身为例,可以使用下面的命令执行打包:
pyodps-pack git+https://github.com/aliyun/aliyun-odps-python-sdk.git
如果想要打包某个分支或者 Tag,可以使用
pyodps-pack git+https://github.com/aliyun/aliyun-odps-python-sdk.git@v0.11.2.2
如果打包前需要安装一些打包依赖(例如 cython
),可以使用 --install-requires
参数增加安装时依赖。
也可以编写一个格式与 requirements.txt
相同的 install-requires.txt
文件,并使用
--install-requires-file
选项指定。例如,如果需要先安装 Cython
再打包 PyODPS,可以写
pyodps-pack \
--install-requires cython \
git+https://github.com/aliyun/aliyun-odps-python-sdk.git@v0.11.2.2
也可以创建一个 install-requires.txt
文件并编写:
cython>0.29
打包命令可以写成
pyodps-pack \
--install-requires-file install-requires.txt \
git+https://github.com/aliyun/aliyun-odps-python-sdk.git@v0.11.2.2
更复杂的例子:二进制依赖
一部分包包含额外的二进制依赖,例如需要编译 / 安装的外部动态链接库。pyodps-pack
提供了
--run-before
参数用以指定打包前需要执行的步骤,该步骤中可以安装所需的二进制依赖。
我们用地理信息库 GDAL 来说明如何打包。
首先确定打包时需要安装的二进制依赖。根据 GDAL 3.6.0.1 在 PyPI 上的文档
,我们需要安装 3.6.0 以上版本的 libgdal。 libgdal 的编译说明
则指出,该包依赖 6.0 以上的 PROJ 包,这两个二进制包均使用 CMake 打包。据此,编写二进制包安装文件并保存为 install-gdal.sh
:
#!/bin/bash
set -e
cd /tmp
curl -o proj-6.3.2.tar.gz https://download.osgeo.org/proj/proj-6.3.2.tar.gz
tar xzf proj-6.3.2.tar.gz
cd proj-6.3.2
mkdir build && cd build
cmake ..
cmake --build .
cmake --build . --target install
cd /tmp
curl -o gdal-3.6.0.tar.gz http://download.osgeo.org/gdal/3.6.0/gdal-3.6.0.tar.gz
tar xzf gdal-3.6.0.tar.gz
cd gdal-3.6.0
mkdir build && cd build
cmake ..
cmake --build .
cmake --build . --target install
此后,使用 pyodps-pack
进行打包:
pyodps-pack --install-requires oldest-supported-numpy --run-before install-gdal.sh gdal==3.6.0.1
在某些情况下,二进制依赖被通过动态链接(例如使用 ctypes.cdll.LoadLibrary
)引入到 Python
中。此时,你可以使用 --dynlib
参数指定需要包含在包中的二进制依赖路径(或者 /lib
下的包名),该依赖将被打包到 packages/dynlibs
路径下。例如,Python 库 unrar
动态链接了 libunrar
这个二进制库,我们使用下面的 install-libunrar.sh
代码编译和安装:
#!/bin/bash
curl -o unrar.tar.gz https://www.rarlab.com/rar/unrarsrc-6.0.3.tar.gz
tar xzf unrar.tar.gz
cd unrar
make -j4 lib
# 该步骤设置输出包 SONAME 为 libunrar.so,为 LoadLibrary 所必需
# 对于大部分二进制包,此步骤可能并非必需
patchelf --set-soname libunrar.so libunrar.so
make install-lib
此后,使用 pyodps-pack
进行打包:
pyodps-pack --run-before install-libunrar.sh --dynlib unrar unrar
在上述命令中, --dynlib
的值 unrar
省略了 lib 前缀, pyodps-pack
实际找到的是
/lib/libunrar.so
。如果有多个动态链接库, --dynlib
可被指定多次。
由于动态链接库的复杂性,你可能需要在 import 你的三方库前手动加载动态链接库,例如
import ctypes
ctypes.cdll.LoadLibrary("work/packages.tar.gz/packages/dynlibs/libunrar.so")
import unrar
对 LoadLibrary
路径的具体说明请参考 Python UDF 使用三方包
中的说明。
命令详情
下面给出 pyodps-pack
命令的可用参数,可用于控制打包过程:
-r
,--requirement <file>
根据给定的依赖文件打包。该选项可被指定多次。
-o
,--output <file>
指定打包生成目标文件名,默认为
packages.tar.gz
。--install-requires <item>
指定打包时所需的 PyPI 依赖,可指定多个。这些依赖 不一定 会包含在最终的包中。
--install-requires-file <file>
指定打包时所需的 PyPI 依赖定义文件,可指定多个。这些依赖 不一定 会包含在最终的包中。
--run-before <script-file>
指定打包前需要执行的 Bash 脚本,通常可用于安装二进制依赖。
-X
,--exclude <dependency>
指定打包时需要从最终包删除的 PyPI 依赖。该选项可被指定多次。
--no-deps
指定打包时不包含指定项目的依赖项。
--pre
如果指定,则打包预发布和开发版本。默认情况下,只包含正式版本。
--proxy <proxy>
指定打包所用的代理服务器,以 scheme://[user:passwd@]proxy.server:port 这样的形式。
--retries <retries>
指定每次连接时的最大重试次数(默认5次)。
timeout <secs>
指定套接字超时时间(默认15秒)。
-i
,--index-url <url>
指定打包时所需的仓库索引 URL。如果缺省,会使用
pip config list
命令返回的global.index-url
值,该值通常配置在pip.conf
配置文件中。--extra-index-url <url>
指定除
--index-url
之外需要使用的仓库索引 URL,规则与--index-url
类似。--trusted-host <host>
指定打包时需要忽略证书问题的 HTTPS 域名。
-l
,--legacy-image
指定后,将使用 CentOS 5 镜像进行打包,这使得包可以被用在旧版专有云等环境中。
--mcpy27
指定后,将为 MaxCompute 内的 Python 2.7 制作三方包。如果启用,将默认
--legacy-image
选项开启。--dwpy27
指定后,将为 DataWorks 内的 Python 2.7 制作三方包。如果启用,将默认
--legacy-image
选项开启。--prefer-binary
指定后,将倾向于选择 PyPI 中包含二进制编译的旧版而不是仅有源码包的新版。
--arch <architecture>
指定目标包面向的硬件架构,目前仅支持 x86_64 和 aarch64(或 arm64),默认为 x86_64。如果你并不在专有云使用 MaxCompute 或 DataWorks,不要指定这个参数。
--python-version <version>
指定目标面向的 Python 版本,可使用 3.6 或者 36 表示 Python 3.6。如果你并不在专有云使用 MaxCompute 或 DataWorks,不要指定这个参数。
--dynlib <lib-name>
指定后,将引入 .so 动态链接库,可以指定具体路径,也可以指定库名(包含或不包含 lib 前缀均可)。
pyodps-pack
将在/lib、/lib64、/usr/lib、/usr/lib64中查找对应库,并置入包中 packages/dynlibs 下。你可能需要手动调用ctypes.cdll.LoadLibrary
在相应包路径引用这些库。--docker-args <args>
指定在执行 Docker 命令时需要额外附加的参数。如有多个参数需用引号包裹,例如
--docker-args "--ip 192.168.1.10"
。--no-docker
使用无 Docker 模式运行
pyodps-pack
。当依赖中存在二进制依赖,可能报错或导致包不可用。--no-merge
下载或生成 Wheel 包后不生成
.tar.gz
包而是保留.whl
文件。--skip-scan-pkg-resources
在打包过程中不在包中扫描和解决
pkg_resources
的依赖,当依赖项较多时可加快打包速度。--find-vcs-root
当需要打包的本地代码需要依赖 Git 等版本管理工具上的 Tag 作为版本信息(例如使用
setuptools_scm
管理版本号)且 Python 包根目录与代码根目录不一致时,该选项能自动向上找到版本管理工具中代码的根目录。--debug
指定后,将输出命令运行的详细信息,用于排查问题。
除此之外,还有若干环境变量可供配置:
DOCKER_PATH="path to docker installation"
指定 Docker 可执行文件路径,路径下需要包括
docker
可执行文件。BEFORE_BUILD="command before build"
指定打包前需要执行的命令。
AFTER_BUILD="command after build"
指定编译后生成 Tar 包前需要执行的命令。
DOCKER_IMAGE="quay.io/pypa/manylinux2010_x86_64"
自定义需要使用的 Docker Image。建议基于
pypa/manylinux
系列镜像定制自定义打包用 Docker Image。
使用三方包
上传三方包
使用三方包前,请确保你生成的包被上传到 MaxCompute Archive 资源。可以使用下面的代码上传资源。 需要注意的是,你需要将 packages.tar.gz 替换成你刚生成的包所在的路径和文件名:
import os
from odps import ODPS
# 确保 ALIBABA_CLOUD_ACCESS_KEY_ID 环境变量设置为用户 Access Key ID,
# ALIBABA_CLOUD_ACCESS_KEY_SECRET 环境变量设置为用户 Access Key Secret,
# 不建议直接使用 Access Key ID / Access Key Secret 字符串
o = ODPS(
os.getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'),
os.getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'),
project='**your-project**',
endpoint='**your-endpoint**',
)
o.create_resource("test_packed.tar.gz", "archive", fileobj=open("packages.tar.gz", "rb"))
也可以使用 DataWorks 上传。具体步骤为:
进入数据开发页面。
登录 DataWorks 控制台。
在左侧导航栏,单击工作空间列表。
选择工作空间所在地域后,单击相应工作空间后的进入数据开发。
鼠标悬停至新建图标,单击MaxCompute > 资源 > Archive
也可以展开业务流程目录下的目标业务流程,右键单击 MaxCompute,选择新建 > 资源 > Archive
在新建资源对话框中,输入资源名称,并选择目标文件夹。
单击点击上传,选择相应的文件进行上传。
单击确定。
单击工具栏中的提交图标,提交资源至调度开发服务器端。
更详细的细节请参考 这篇文章 。
在 Python UDF 中使用三方包
你需要对你的 UDF 进行修改以使用上传的三方包。具体地,你需要在 UDF 类的 __init__
方法中添加对三方包的引用,
然后再在UDF代码中(例如 evaluate / process 方法)调用三方包。
我们以实现 scipy 中的 psi 函数为例展示如何在 Python UDF 中使用三方包。首先使用下面的命令打包:
pyodps-pack -o scipy-bundle.tar.gz scipy
随后编写下面的代码,并保存为 test_psi_udf.py
:
import sys
from odps.udf import annotate
@annotate("double->double")
class MyPsi(object):
def __init__(self):
# 如果依赖中包含 protobuf,需要添加下面这行语句,否则不需要
sys.setdlopenflags(10)
# 将路径增加到引用路径
sys.path.insert(0, "work/scipy-bundle.tar.gz/packages")
def evaluate(self, arg0):
# 将 import 语句保持在 evaluate 函数内部
from scipy.special import psi
return float(psi(arg0))
对上面的代码做一些解释。
当依赖中包含 protobuf 时,需要为
__init__
函数增加sys.setdlopenflags(10)
(pyodps-pack
打包过程中会提示),该设置可以避免三方包和 MaxCompute 间相关的版本冲突。__init__
函数中将work/scipy-bundle.tar.gz/packages
添加到sys.path
, 因为 MaxCompute 会将所有 UDF 引用的 Archive 资源以资源名称为目录解压到work
目录下,而packages
则是pyodps-pack
生成包的子目录。如果你需要通过LoadLibrary
引入--dynlib
参数引入的动态链接库,也可以在此处引用。将对 scipy 的 import 放在 evaluate 函数体内部的原因是三方包仅在执行时可用,当 UDF 在 MaxCompute 服务端被解析时,解析环境不包含三方包,函数体外的三方包 import 会导致报错。
随后需要将 test_psi_udf.py
上传为 MaxCompute Python 资源,以及将 scipy-bundle.tar.gz
上传为 Archive
资源。此后,创建 UDF 名为 test_psi_udf
,引用上面两个资源文件,并指定类名为 test_psi_udf.MyPsi
。
利用 PyODPS 完成上述步骤的代码为
import os
from odps import ODPS
# 确保 ALIBABA_CLOUD_ACCESS_KEY_ID 环境变量设置为用户 Access Key ID,
# ALIBABA_CLOUD_ACCESS_KEY_SECRET 环境变量设置为用户 Access Key Secret,
# 不建议直接使用 Access Key ID / Access Key Secret 字符串
o = ODPS(
os.getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'),
os.getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'),
project='**your-project**',
endpoint='**your-endpoint**',
)
bundle_res = o.create_resource(
"scipy-bundle.tar.gz", "archive", fileobj=open("scipy-bundle.tar.gz", "rb")
)
udf_res = o.create_resource(
"test_psi_udf.py", "py", fileobj=open("test_psi_udf.py", "rb")
)
o.create_function(
"test_psi_udf", class_type="test_psi_udf.MyPsi", resources=[bundle_res, udf_res]
)
使用 MaxCompute Console 上传的方法为
add archive scipy-bundle.tar.gz;
add py test_psi_udf.py;
create function test_psi_udf as test_psi_udf.MyPsi using test_psi_udf.py,scipy-bundle.tar.gz;
完成上述步骤后,即可使用 UDF 执行 SQL:
set odps.pypy.enabled=false;
set odps.isolation.session.enable=true;
select test_psi_udf(sepal_length) from iris;
在 PyODPS DataFrame 中使用三方包
PyODPS DataFrame 支持在 execute / persist 时使用 libraries 参数使用上面的第三方库。 下面以 map 方法为例,apply / map_reduce 方法的过程类似。
首先,用下面的命令打包 scipy:
pyodps-pack -o scipy-bundle.tar.gz scipy
假定我们的表名为 test_float_col
,内容只包含一列 float 值:
col1
0 3.75
1 2.51
计算 psi(col1) 的值,可以编写下面的代码:
import os
from odps import ODPS, options
def psi(v):
from scipy.special import psi
return float(psi(v))
# 如果 Project 开启了 Isolation,下面的选项不是必需的
options.sql.settings = {"odps.isolation.session.enable": True}
# 确保 ALIBABA_CLOUD_ACCESS_KEY_ID 环境变量设置为用户 Access Key ID,
# ALIBABA_CLOUD_ACCESS_KEY_SECRET 环境变量设置为用户 Access Key Secret,
# 不建议直接使用 Access Key ID / Access Key Secret 字符串
o = ODPS(
os.getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'),
os.getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'),
project='**your-project**',
endpoint='**your-endpoint**',
)
df = o.get_table("test_float_col").to_df()
# 直接执行并取得结果
df.col1.map(psi).execute(libraries=["scipy-bundle.tar.gz"])
# 保存到另一张表
df.col1.map(psi).persist("result_table", libraries=["scipy-bundle.tar.gz"])
如果希望在整个代码执行过程中使用相同的三方包,可以设置全局选项:
from odps import options
options.df.libraries = ["scipy-bundle.tar.gz"]
此后即可在 DataFrame 执行时用到相关的三方包。
在 DataWorks 中使用三方包
DataWorks PyODPS 节点预装了若干三方包,同时提供了 load_resource_package
方法用以引用其他的包,
具体使用方式可参考 这里。
手动上传和使用三方包
备注
以下内容仅作为维护旧项目或者旧环境的参考,新项目建议直接使用 pyodps-pack
打包。
部分旧项目可能使用了之前的方式使用三方包,即手动上传所有依赖的 Wheel 包并在代码中引用,或者使用了不支持二进制包的旧版 MaxCompute 环境,本章节为这部分场景准备。下面以在 map 中使用 python_dateutil 为例说明使用三方包的步骤。
首先,我们可以在 Linux bash 中使用 pip download
命令,下载包以及其依赖到某个路径。
这里下载后会出现两个包:six-1.10.0-py2.py3-none-any.whl和python_dateutil-2.5.3-py2.py3-none-any.whl
(这里注意需要下载支持 Linux 环境的包,建议直接在 Linux 下调用该命令。)
pip download python-dateutil -d /to/path/
然后我们分别把两个文件上传到ODPS资源
# 这里要确保资源名的后缀是正确的文件类型
odps.create_resource('six.whl', 'file', file_obj=open('six-1.10.0-py2.py3-none-any.whl', 'rb'))
odps.create_resource('python_dateutil.whl', 'file', file_obj=open('python_dateutil-2.5.3-py2.py3-none-any.whl', 'rb'))
现在我们有个DataFrame,只有一个string类型字段。
>>> df
datestr
0 2016-08-26 14:03:29
1 2015-08-26 14:03:29
全局配置使用到的三方库:
>>> from odps import options
>>>
>>> def get_year(t):
>>> from dateutil.parser import parse
>>> return parse(t).strftime('%Y')
>>>
>>> options.df.libraries = ['six.whl', 'python_dateutil.whl']
>>> df.datestr.map(get_year)
datestr
0 2016
1 2015
或者,通过立即运行方法的 libraries
参数指定:
>>> def get_year(t):
>>> from dateutil.parser import parse
>>> return parse(t).strftime('%Y')
>>>
>>> df.datestr.map(get_year).execute(libraries=['six.whl', 'python_dateutil.whl'])
datestr
0 2016
1 2015
PyODPS 默认支持执行纯 Python 且不含文件操作的第三方库。在较新版本的 MaxCompute 服务下,PyODPS 也支持执行带有二进制代码或带有文件操作的 Python 库。这些库名必须拥有一定的后缀,可根据下表判断
平台 |
Python 版本 |
可用的后缀 |
RHEL 5 x86_64 |
Python 2.7 |
cp27-cp27m-manylinux1_x86_64 |
RHEL 5 x86_64 |
Python 3.7 |
cp37-cp37m-manylinux1_x86_64 |
RHEL 7 x86_64 |
Python 2.7 |
cp27-cp27m-manylinux1_x86_64, cp27-cp27m-manylinux2010_x86_64, cp27-cp27m-manylinux2014_x86_64 |
RHEL 7 x86_64 |
Python 3.7 |
cp37-cp37m-manylinux1_x86_64, cp37-cp37m-manylinux2010_x86_64, cp37-cp37m-manylinux2014_x86_64 |
RHEL 7 ARM64 |
Python 3.7 |
cp37-cp37m-manylinux2014_aarch64 |
所有的 whl 包都需要以 archive 格式上传,whl 后缀的包需要重命名为 zip。同时,作业需要开启
odps.isolation.session.enable
选项,或者在 Project 级别开启 Isolation。下面的例子展示了如何上传并使用
scipy 中的特殊函数:
# 对于含有二进制代码的包,必须使用 Archive 方式上传资源,whl 后缀需要改为 zip
odps.create_resource('scipy.zip', 'archive', file_obj=open('scipy-0.19.0-cp27-cp27m-manylinux1_x86_64.whl', 'rb'))
# 如果 Project 开启了 Isolation,下面的选项不是必需的
options.sql.settings = { 'odps.isolation.session.enable': True }
def psi(value):
# 建议在函数内部 import 第三方库,以防止不同操作系统下二进制包结构差异造成执行错误
from scipy.special import psi
return float(psi(value))
df.float_col.map(psi).execute(libraries=['scipy.zip'])
对于只提供源码的二进制包,可以在 Linux Shell 中打包成 Wheel 再上传,Mac 和 Windows 中生成的 Wheel 包无法在 MaxCompute 中使用:
python setup.py bdist_wheel