由于某些不可抗力因素的存在,当我们通过 pip install xxx
的方式安装第三方 Python 包时,经常出现速度不理想的情况,而这个时候通常解决方案是使用诸如 豆瓣源
、阿里源
等来进行下载 Python 包。其实我们也可以自己搭建一个私有的 PyPI 仓库来进行 Python 包管理,本文就来介绍下如何搭建一个私有的 PyPI 仓库。
优势
先解释下已经有了 豆瓣源
、阿里源
的存在,我们为什么还要自己搭建 PyPI 仓库,自建 PyPI 仓库有哪些优势:
速度更快,如果自己搭建的 PyPI 仓库只在内网使用,那么理论速度将会更快。
可掌控,第三方 PyPI 源一般都是定时同步 Python 官方的 PyPI 仓库,有时候会出现某些包不存在的情况,如果是我们自己搭建的私有 PyPI 仓库,则可以自己手动上传 Python 包。
隐私性,有些内部库不打算开源,但需要团队之间共享,如果只用私有
Git
仓库来管理分发则装包时就会比较麻烦,既然是私有 PyPI 仓库,我们就可以控制只有内部人员可以使用。
搭建并使用
我们将使用 pypiserver
开源项目来搭建私有 PyPI 仓库,pypiserver
使用 Bottle
这个 Python Web 框架实现了一个轻量版的 PyPI。
pypiserver
搭建方式有多种,可以直接通过 pip install pypiserver
来安装并使用,不过我今天想要介绍的是使用 Docker 的方式来安装。
启动 pypiserver
1 | ➜ docker run --rm -p 80:8080 pypiserver/pypiserver:latest -P . -a . --fallback-url https://pypi.douban.com/simple |
各参数说明:
docker run
表示通过 Docker 启动一个容器(进程)--rm
表示这个容器用完即删,非常适合测试阶段-p 80:8080
表示主机监听到80
端口的请求将会转发给容器内部的8080
端口,也就是会被pypiserver
接收到pypiserver/pypiserver:latest
表示容器镜像,版本为latest
-P . -a .
是启动pypiserver
的参数,表名不使用用户名和密码进行认证--fallback-url https://pypi.douban.com/simple
下载包时,如果在私有 PyPI 仓库中没有找到某个包,将请求转发到豆瓣源
启动后访问 http://127.0.0.1/
将得打如下界面:
表示已经成功搭建私有 PyPI 仓库。
上传 Python 包到 pypiserver
以 python_packaging_tutorial
项目为例,将其打包并上传到 pypiserver
。
项目目录结构如下:
1 | python_packaging_tutorial |
构建项目包
构建命令如下:
1 | # 安装构建工具 |
现在目录结构如下:
1 | python_packaging_tutorial |
可以看到在 dist
目录下生成了两个包文件 example_package-0.0.1.tar.gz
、example_package-0.0.1-py3-none-any.whl
。前者只对项目代码进行了打包和压缩工作,后者是编译好的二进制包。为了保证跨平台性,我们这里只会将 example_package-0.0.1.tar.gz
上传到 pypiserver
。
Tip: 如果想在本地直接安装构建好的包,执行 python3 -m pip install ./dist/example_package-0.0.1.tar.gz
命令即可。
上传项目包
创建 ~/.pypirc
文件,供下面介绍的分发工具 twine
来使用
1 | [distutils] |
上传构建好的 Python 包
1 | # Python 现在推荐使用 twine 上传分发包,所以需要先安装它 |
现在访问 http://127.0.0.1/simple/example-package/
将看到已经上传成功的包
从 pypiserver
下载并使用包
有如下测试代码 test_package.py
:
1 | from example_package import example |
从私有 PyPI 仓库安装 example_package
1 | ➜ pip install example-package -i http://127.0.0.1/simple/ |
注意: example_package
项目依赖了外部第三方包,在 pyproject.toml
可以看到如下配置:
1 | dependencies = [ |
我们搭建的私有 PyPI 仓库只上传了 example_package
包,并不包含以上这几个外部第三方包,当在 pypiserver
中找不到要下载的包时,请求会被转发到 --fallback-url
指定的镜像源地址。
执行测试程序
1 | ➜ python3 test_package.py |
没有问题。
启用认证
生成认证文件
现在虽然已经通过 pypiserver
搭建并使用了私有 PyPI 仓库,但还不够私有化,用现在比较流行的说法:私有了,但没完全私有。因为只要在同一内网中的其他用户就可以随意将自己的 Python 包上传到 pypiserver
中,这可能会导致有人随意上传和 example_package
同名的包,来覆盖我们自己上传的 Python 包。
现在我们需要给 pypiserver
增加认证功能,这样只有拥有账号密码的用户才可以上传 Python 包。
这里需要借助 Apache htpasswd
来生成一个认证文件供 pypiserver
服务使用
1 | ➜ htpasswd -c ~/.htpasswd username |
使用 htpasswd
来生成一个用户名为 username
密码为 password
的认证文件 ~/.htpasswd
。
其文件内容如下:
1 | ➜ cat ~/.htpasswd |
启动带有认证功能的 pypiserver
先使用 Ctrl+C
停掉之前使用 docker run ...
命令启动的 pypiserver
服务,还记得启动时指定的 --rm
参数吗,这个参数此时就会发挥作用,它会自动删除当前停掉的容器。
再使用如下命令启动新的 pypiserver
服务:
1 | ➜ docker run --rm -p 80:8080 -v ~/.htpasswd:/data/.htpasswd pypiserver/pypiserver:latest --fallback-url https://pypi.douban.com/simple -P .htpasswd packages |
这次的启动命令先通过 -v ~/.htpasswd:/data/.htpasswd
参数将认证文件 ~/.htpasswd
复制到容器内部,然后再通过 -P .htpasswd
来供 pypiserver
服务使用,就可以开启私有 PyPI 仓库的认证功能。最后的 packages
参数指明存放 Python 包的目录,不指定也没关系,它必须是容器内部已经存在的目录,默认为 /data/packages
目录。
现在上传 Python 包则需要指明用户名和密码才可以上传成功:
1 | ➜ python3 -m twine upload --repository private-pypi dist/example_package-0.0.1.tar.gz -u username -p password |
否则将得到如下错误:
1 | ➜ python3 -m twine upload --repository private-pypi dist/example_package-0.0.1.tar.gz -u "" -p "" |
优化认证过程
你一定不想每次使用私有 PyPI 仓库上传 Python 包都输入用户名和密码,即使通过使用环境变量来保存用户名和密码的方式也会比较繁琐,不过有一种非常简单的方法可以做到避免输入用户名和密码的麻烦。
修改 ~/.pypirc
文件:
1 | [distutils] |
对应的 <username>
、<password>
部分替换成我们用 .htpasswd
创建认证文件时指定的用户名和密码
现在直接使用如下命令即可上传 Python 包到私有仓库:
1 | ➜ python3 -m twine upload --repository private-pypi dist/example_package-0.0.1.tar.gz |
总结
本文介绍了使用 Docker 搭建私有 PyPI 仓库的基本流程,如果正式部署 pypiserver
项目,则在使用 docker run ...
命令启动的 pypiserver
服务时,建议使用如下命令:
1 | ➜ docker run -d --name pypiserver -p 80:8080 -v ~/packages:/data/packages -v ~/.htpasswd:/data/.htpasswd pypiserver/pypiserver:latest --fallback-url https://pypi.douban.com/simple -P .htpasswd packages |
这条命令将
--rm
参数移除,这样当想要停止或者重启容器时容器不会被清理掉新增加的
-d
参数让容器以后台进程的方式运行,这样即使关闭终端,容器依然在运行--name pypiserver
参数给启动容器取名为pypiserver
,方便维护-v ~/packages:/data/packages
参数将主机~/packages
目录挂载到容器/data/packages
目录,这样即使删除容器,我们上传过的 Python 包依然存在,不会丢失,重新运行新的容器时再次使用此参数挂载容器,那么新启动的pypiserver
服务依然能够读取以前上传的 Python 包
如果你对 Docker 不熟悉,官方文档也有直接通过 pip
安装 pypiserver
的方式。如果需要更高级的功能可以在 pypiserver
GitHub 官方仓库查看更详细的文档。
如果你有多个私有仓库需要配置,那么 ~/.pypirc
文件可以这样写:
1 | [distutils] |
本文也顺带讲了一点构建 Python 包的知识,你也许对使用 pyproject.toml
文件来构建 Python 包比较陌生,现在较知名的 Python 开源项目由于历史原因,使用此方式的的确不多,不过还是推荐你了解一下。关于 Python 包构建可讲的东西其实也比较多,简单一句话概括:setup.py
是过去,setup.cfg
是现在,pyproject.toml
是未来。
希望这篇文档对你有所帮助。
参考
https://packaging.python.org/en/latest/tutorials/packaging-projects/
https://peps.python.org/pep-0631/
https://peps.python.org/pep-0517/