由于某些不可抗力因素的存在,当我们通过 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/
将得打如下界面:
Welcome to pypiserver
表示已经成功搭建私有 PyPI 仓库。
上传 Python 包到 pypiserver
以 python_packaging_tutorial
项目为例,将其打包并上传到 pypiserver
。
项目目录结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
python_packaging_tutorial ├── LICENSE ├── README.md ├── pyproject.toml ├── requirements.txt ├── src │ └── example_package │ ├── __init__.py │ └── example.py └── tests ├── __init__.py └── test_example.py |
构建项目包
构建命令如下:
1 2 3 4 |
# 安装构建工具 ➜ python3 -m pip install --upgrade build # 构建(打包) ➜ python3 -m build |
现在目录结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
python_packaging_tutorial ├── LICENSE ├── README.md ├── dist │ ├── example_package-0.0.1-py3-none-any.whl │ └── example_package-0.0.1.tar.gz ├── pyproject.toml ├── requirements.txt ├── src │ ├── example_package │ │ ├── __init__.py │ │ └── example.py │ └── example_package.egg-info │ ├── PKG-INFO │ ├── SOURCES.txt │ ├── dependency_links.txt │ ├── requires.txt │ └── top_level.txt └── tests ├── __init__.py └── test_example.py |
可以看到在 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 2 3 4 5 |
[distutils] index-servers=private-pypi [private-pypi] repository = http://127.0.0.1:80 |
上传构建好的 Python 包
1 2 3 4 5 6 |
# Python 现在推荐使用 twine 上传分发包,所以需要先安装它 ➜ python3 -m pip install --upgrade twine # 上传 Python 包到 PyPI 仓库 # --repository private-pypi 指定私有 PyPI 仓库,从 `~/.pypirc` 中读取 URL 地址 # -u "" -p "" 分别指定用户名和密码为空 ➜ python3 -m twine upload --repository private-pypi dist/example_package-0.0.1.tar.gz -u "" -p "" |
现在访问 http://127.0.0.1/simple/example-package/
将看到已经上传成功的包
Example Package
从 pypiserver
下载并使用包
有如下测试代码 test_package.py
:
1 2 3 4 5 6 |
from example_package import example example.hello_world() response = example.httpbin_get() print(response.status_code) |
从私有 PyPI 仓库安装 example_package
1 |
➜ pip install example-package -i http://127.0.0.1/simple/ |
注意:example_package
项目依赖了外部第三方包,在 pyproject.toml
可以看到如下配置:
1 2 3 4 5 6 7 |
dependencies = [ "certifi==2022.6.15", "charset-normalizer==2.1.1", "idna==3.3", "requests==2.28.1", "urllib3==1.26.12", ] |
我们搭建的私有 PyPI 仓库只上传了 example_package
包,并不包含以上这几个外部第三方包,当在 pypiserver
中找不到要下载的包时,请求会被转发到 --fallback-url
指定的镜像源地址。
执行测试程序
1 2 3 |
➜ python3 test_package.py Hello World! 200 |
没有问题。
启用认证
生成认证文件
现在虽然已经通过 pypiserver
搭建并使用了私有 PyPI 仓库,但还不够私有化,用现在比较流行的说法:私有了,但没完全私有。因为只要在同一内网中的其他用户就可以随意将自己的 Python 包上传到 pypiserver
中,这可能会导致有人随意上传和 example_package
同名的包,来覆盖我们自己上传的 Python 包。
现在我们需要给 pypiserver
增加认证功能,这样只有拥有账号密码的用户才可以上传 Python 包。
这里需要借助 Apache htpasswd
来生成一个认证文件供 pypiserver
服务使用
1 2 3 4 |
➜ htpasswd -c ~/.htpasswd username New password: # 输入密码 Re-type new password: # 再次输入密码 Adding password for user username |
使用 htpasswd
来生成一个用户名为 username
密码为 password
的认证文件 ~/.htpasswd
。
其文件内容如下:
1 2 |
➜ cat ~/.htpasswd username:$apr1$Yob6NUQf$M8vpxJ.PpNYh0FEahMLzS0 |
启动带有认证功能的 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 2 3 4 5 6 7 |
➜ python3 -m twine upload --repository private-pypi dist/example_package-0.0.1.tar.gz -u "" -p "" Uploading distributions to http://127.0.0.1:80 Uploading example_package-0.0.1.tar.gz 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 7.0/7.0 kB • 00:00 • ? WARNING Error during upload. Retry with the --verbose option for more details. ERROR HTTPError: 401 Unauthorized from http://127.0.0.1:80/ Unauthorized |
优化认证过程
你一定不想每次使用私有 PyPI 仓库上传 Python 包都输入用户名和密码,即使通过使用环境变量来保存用户名和密码的方式也会比较繁琐,不过有一种非常简单的方法可以做到避免输入用户名和密码的麻烦。
修改 ~/.pypirc
文件:
1 2 3 4 5 6 7 |
[distutils] index-servers=private-pypi [private-pypi] repository = http://127.0.0.1:80 username = <username> password = <password> |
对应的 <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 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[distutils] index-servers = local 5.14 [local] repository = http://127.0.0.1:8080 username = username password = password [5.14] repository = http://10.0.5.14:31578 username = fake password = fake |
本文也顺带讲了一点构建 Python 包的知识,你也许对使用 pyproject.toml
文件来构建 Python 包比较陌生,现在较知名的 Python 开源项目由于历史原因,使用此方式的的确不多,不过还是推荐你了解一下。关于 Python 包构建可讲的东西其实也比较多,简单一句话概括:setup.py
是过去,setup.cfg
是现在,pyproject.toml
是未来。
希望这篇文档对你有所帮助。
参考
https://packaging.python.org/en/latest/tutorials/packaging-projects/