これは、なにをしたくて書いたもの?
Sonatype Nexus 3でPyPiリポジトリーを作成して、Pythonパッケージを公開してみます。
Pythonパッケージと配布まわり
Pythonのパッケージと配布に関する知識がないので、まずはこちらを見ていこうと思います。
Pythonパッケージング
Pythonのパッケージングについては、こちらのドキュメントを見るのがよさそうです。
Overview of Python Packaging - Python Packaging User Guide
以下の2種類があるようです。
- Overview of Python Packaging / Packaging Python libraries and tools / Python source distributions
- Overview of Python Packaging / Packaging Python libraries and tools / Python binary distributions
sdistとwheelは、両方とも公開することが望ましいされています。wheelのソースコードがsdistなので、利用者の環境に対応する
wheelがなくてもsdistからビルドできる可能性があるからです。またpipでパッケージをインストールする際に、sdistとwheelの
両方が存在する場合はデフォルトでwheelが優先されます。
各配布物の仕様はこちら。
- Source distribution format - Python Packaging User Guide
- Binary distribution format - Python Packaging User Guide
Python以外の言語で書かれたライブラリーを含むwheelの場合、ビルド成果物に対応するPythonのバージョンや
CPUアーキテクチャー、プラットフォームに応じたタグが使われます。なので、組み合わせがたくさんになることが
あります。
Platform compatibility tags - Python Packaging User Guide
sdistとwheelの両方をアップロードするには、PyPiを使うのがよいとされているようです。
パッケージングと利用の流れ
パッケージングと、公開されたパッケージを利用する流れは以下に書かれています。
The Packaging Flow - Python Packaging User Guide
概ね、以下の流れです。
- ソースコードを作成する
- 設定ファイルを作成する
- 標準的には
pyproject.toml build-systemでビルドツールを指定する- 実際にビルドを行うツールを「バックエンド」と呼ぶ
- flit、hatch、pdm、Poetry、Setuptools、trampolim、wheyなど
- ユーザーがビルドコマンドを実行するツールを「フロントエンド」と呼ぶ
- pip、buildなど
- 標準的には
- ビルド成果物を作成する
- sdist(たとえば
python3 -m build --sdist source-tree-directory) - wheel(たとえば
python3 -m build --wheel source-tree-directory)
- sdist(たとえば
- パッケージ配信サービス(たとえばPyPi)へアップロードする
- ここではtwineのようなツールを使用する
- パッケージをダウンロード、インストールする
- pip installなど
この流れを体感するのがチュートリアルですね。
ところで、こちらを見るとパッケージのインストール元はパッケージ配信サービスだけではなく、ローカルファイルシステムや
VCSを選ぶこともできるようです。
Installing Packages - Python Packaging User Guide
またパッケージを探すためにインデックスが必要なようですね。
python3 -m pip install --index-url http://my.package.repo/simple/ SomeProject
ガイド
詳しいガイドはこちらですね。
Guides - Python Packaging User Guide
ビルドや公開についてはこちら。
Building and Publishing - Python Packaging User Guide
Sonatype Nexus 3のPyPiリポジトリー
ここまで読んできて、やっとSonatype Nexus 3のPyPiリポジトリーに話が移せます。
他のリポジトリーと同じように、Proxy、Hosted、Groupリポジトリーについて書かれています。
いくつか気になることが書かれています。
Proxyリポジトリーを使う時は、対象が/simpleエンドポイントをサポートしている必要があります。
Nexus Repository currently requires the remote PyPI repository to support the /simple endpoint.
PyPI Repositories / Proxying PyPI Repositories
サポートしているPython、pip、setuptoolsは、特定のバージョンのようです。Pythonについては2と3の直近2つのバージョンの
ようです。
Nexus Repository only supports specific versions of Python, pip, and setuptools. For Python, only the latest two releases of 2 and 3 are supported.
PyPI Repositories / Installing PyPI Client Tools
とはいえ、具体的にどのバージョンをサポートしているのかはハッキリと書かれてはいないのですが…。
パッケージのアップロードに使用するツールによって、リポジトリーの構成が影響を受けるようです。
Depending on your preference for twine, distutils, pip, and setuptools, your proxy and hosted configuration will vary.
PyPI Repositories / Configuring PyPI Client Tools
uvでパッケージをデプロイする(公開する)
uvを使ってパッケージを公開する方法は、こちらのページに書かれています。
Building and publishing a package | uv
このあたりも関係ありそうです。
- Configuring projects / Build systems
- Configuring projects / Project packaging
- Building distributions | uv
ひとまず試してみましょうか。
環境
今回の環境はこちら。
flowchart LR
subgraph Host1/192.168.33.10
A["uv/Project1"]
B["uv/Project2"]
end
subgraph Host2/192.168.33.11
C["Sonatype Nexus"]
A -- "publish/HTTP" --> C
C -- "install/HTTP" --> B
end
Pythonまわり。
$ python3 --version Python 3.12.3 $ uv --version uv 0.8.4
Sonatype Nexusは3.82.0-08を使います。
また、環境構築はTerraformで行います。
$ terraform version Terraform v1.12.2 on linux_amd64
お題
今回のお題は、あるuvプロジェクトで作成したパッケージをSonatype NexusのPyPiリポジトリーに公開し、別のuvプロジェクトで
使う、というものにします。
内容としては、このブログをスクレイピングするライブラリーにしましょう。
PyPiリポジトリーの作成
Terraform構成ファイル。
main.tf
terraform { required_version = "v1.12.2" required_providers { nexus = { source = "datadrivers/nexus" version = "2.6.0" } } } provider "nexus" { username = "admin" password = "admin123" url = "http://192.168.33.11:8081" insecure = true } resource "nexus_repository_pypi_proxy" "pypi_proxy" { name = "pypi-proxy" online = true storage { blob_store_name = "default" strict_content_type_validation = true } proxy { remote_url = "https://pypi.org" content_max_age = 1440 metadata_max_age = 1440 } negative_cache { enabled = true ttl = 1440 } http_client { blocked = false auto_block = true } } resource "nexus_repository_pypi_hosted" "hosted" { name = "pypi-hosted" online = true storage { blob_store_name = "default" strict_content_type_validation = true write_policy = "ALLOW" } } resource "nexus_repository_pypi_group" "group" { name = "pypi-group" online = true group { member_names = [ nexus_repository_pypi_hosted.hosted.name, nexus_repository_pypi_proxy.pypi_proxy.name, ] } storage { blob_store_name = "default" strict_content_type_validation = true } }
Groupリポジトリーが、Proxy、Hostedリポジトリーを束ねる構成です。
ところで、Hostedリポジトリーのwrite_policyに指定する値がなにかわからなかったのですが
resource "nexus_repository_pypi_hosted" "hosted" { name = "pypi-hosted" online = true storage { blob_store_name = "default" strict_content_type_validation = true write_policy = "ALLOW" } }
適当な値を指定すると怒られたのでこれでわかりました…。Sonatype NexusのWeb UIに表示されている値とも違うので、
どこかで確認できるとよいのですが…。
│ Error: expected storage.0.write_policy to be one of ["ALLOW" "ALLOW_ONCE" "DENY"], got ...
ドキュメント上は「Deployment Policy」として書かれているんですけどね、この値はわかりません。
Configurable Repository Fields
$ terraform init $ terraform apply
PyPiパッケージの作成と公開
次は、作成したPyPiリポジトリーに自前のパッケージを公開します。
まずはuvプロジェクトの作成。
$ uv init --vcs none --lib clover-scrape $ uv init --vcs none clover-scrape $ cd clover-scrape
uv initの時に、--libオプションを指定することでライブラリー用の構成になります。
こんな内容ですね。
$ tree
.
├── README.md
├── pyproject.toml
└── src
└── clover_scrape
├── __init__.py
└── py.typed
3 directories, 4 files
デフォルトは--appでアプリケーション用、他には--packageでパッケージ用のプロジェクトを作成できます。パッケージ用の
プロジェクトを選ぶと、CLIを含めた構成になります。
スケルトンで作成されたファイルはこんな感じですね。
src/clover_scrape/__init__.py
def hello() -> str: return "Hello from clover-scrape!"
src/clover_scrape/py.typed
pyproject.toml
[project] name = "clover-scrape" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [] [build-system] requires = ["uv_build>=0.8.4,<0.9.0"] build-backend = "uv_build"
pyproject.tomlにちょっと変わった要素があります。
[build-system] requires = ["uv_build>=0.8.4,<0.9.0"] build-backend = "uv_build"
これはプロジェクトのビルドバックエンドのことです。
Configuring projects / Build systems
つまり、このプロジェクトではビルドバックエンドにuvが設定されていることになります。
ちなみにuvビルドバックエンドは、純粋なPythonコードのみをサポートしているようです。他の言語で実装されたモジュールを
含む場合は、別のビルドバックエンドを使うことになります。
The uv build backend currently only supports pure Python code. An alternative backend is required to build a library with extension modules.
では、プロジェクトに依存関係を追加。
$ uv add requests $ uv add --dev pytest mypy ruff $ uv add --dev types-requests
せっかくなので、外部のライブラリーも使うようにします。
こんなスクリプトを作成。今回はブログのタイトルだけを取ることにします。
src/clover_scrape/__init__.py
import re import requests def get_title() -> str: response = requests.get("https://kazuhira-r.hatenablog.com/") html = response.text match = re.search(r"<title>(.*?)</title>", html) if match: return match.group(1) else: raise RuntimeError("titleが見つかりません")
なんとなく、テストコードも作成。
tests/test_lib.py
from clover_scrape import get_title def test_get_title() -> None: assert get_title() == "CLOVER🍀"
pyproject.tomlは、最終的にこうなりました。
pyproject.toml
[project] name = "clover-scrape" version = "0.0.1" description = "CLOVER scape lib" readme = "README.md" authors = [ { name = "kazuhira-r", email = "kazuhira.m@example.com" } ] requires-python = ">=3.12" dependencies = [ "requests>=2.32.4", ] license = "MIT" [build-system] requires = ["uv_build>=0.8.4,<0.9.0"] build-backend = "uv_build" [[tool.uv.index]] name = "mypypi" url = "http://192.168.33.11:8081/repository/pypi-group/simple/" publish-url = "http://192.168.33.11:8081/repository/pypi-hosted/" [dependency-groups] dev = [ "mypy>=1.17.1", "pytest>=8.4.1", "ruff>=0.12.7", "types-requests>=2.32.4.20250611", ] [tool.mypy] strict = true disallow_any_unimported = true disallow_any_expr = true disallow_any_explicit = true warn_unreachable = true pretty = true [tool.pytest.ini_options] pythonpath = ["src"]
では、プロジェクトをビルドします。
$ uv build Building source distribution (uv build backend)... Building wheel from source distribution (uv build backend)... Successfully built dist/clover_scrape-0.0.1.tar.gz Successfully built dist/clover_scrape-0.0.1-py3-none-any.whl
Building and publishing a package / Building your package
sdistとwheelが作成されたようです。
$ ll dist 合計 20 drwxrwxr-x 2 xxxxx xxxxx 4096 8月 3 18:49 ./ drwxrwxr-x 9 xxxxx xxxxx 4096 8月 3 18:53 ../ -rw-rw-r-- 1 xxxxx xxxxx 1 8月 3 18:49 .gitignore -rw-rw-r-- 1 xxxxx xxxxx 1759 8月 3 18:49 clover_scrape-0.0.1-py3-none-any.whl -rw-rw-r-- 1 xxxxx xxxxx 1051 8月 3 18:49 clover_scrape-0.0.1.tar.gz
中身を確認してみましょう。sdistから。
$ tar tvf dist/clover_scrape-0.0.1.tar.gz -rw-r--r-- 0/0 273 1970-01-01 09:00 clover_scrape-0.0.1/PKG-INFO drwxr-xr-x 0/0 0 1970-01-01 09:00 clover_scrape-0.0.1/ -rw-r--r-- 0/0 0 1970-01-01 09:00 clover_scrape-0.0.1/README.md -rw-r--r-- 0/0 841 1970-01-01 09:00 clover_scrape-0.0.1/pyproject.toml drwxr-xr-x 0/0 0 1970-01-01 09:00 clover_scrape-0.0.1/src drwxr-xr-x 0/0 0 1970-01-01 09:00 clover_scrape-0.0.1/src/clover_scrape -rw-r--r-- 0/0 338 1970-01-01 09:00 clover_scrape-0.0.1/src/clover_scrape/__init__.py -rw-r--r-- 0/0 0 1970-01-01 09:00 clover_scrape-0.0.1/src/clover_scrape/py.typed
wheelはzipファイルのようなので
$ file dist/clover_scrape-0.0.1-py3-none-any.whl dist/clover_scrape-0.0.1-py3-none-any.whl: Zip archive data, at least v2.0 to extract, compression method=store
こちらで確認。
$ unzip -l dist/clover_scrape-0.0.1-py3-none-any.whl
Archive: dist/clover_scrape-0.0.1-py3-none-any.whl
Length Date Time Name
--------- ---------- ----- ----
0 1980-01-01 00:00 clover_scrape/
338 1980-01-01 00:00 clover_scrape/__init__.py
0 1980-01-01 00:00 clover_scrape/py.typed
0 1980-01-01 00:00 clover_scrape-0.0.1.dist-info/
78 1980-01-01 00:00 clover_scrape-0.0.1.dist-info/WHEEL
273 1980-01-01 00:00 clover_scrape-0.0.1.dist-info/METADATA
464 1980-01-01 00:00 clover_scrape-0.0.1.dist-info/RECORD
--------- -------
1153 7 files
ビルドしたらパッケージを公開します。
$ uv publish --index mypypi
Building and publishing a package / Publishing your package
この時、--indexでアップロード先を指定します。
この定義はこちらですね。
[[tool.uv.index]] name = "mypypi" url = "http://192.168.33.11:8081/repository/pypi-group/simple/" publish-url = "http://192.168.33.11:8081/repository/pypi-hosted/"
厳密に言うとこの部分です。Hostedリポジトリーをtool.uv.indexのpublish-urlに指定しています。
[[tool.uv.index]] name = "mypypi" publish-url = "http://192.168.33.11:8081/repository/pypi-hosted/"
アップロードしようとすると、ログインを求められます。
Publishing 2 files http://192.168.33.11:8081/repository/pypi-hosted/
Enter username ('__token__' if using a token): admin
Enter password:
Uploading clover_scrape-0.0.1-py3-none-any.whl (1.7KiB)
Uploading clover_scrape-0.0.1.tar.gz (1.0KiB)
これで完了ですね。
ログイン情報は環境変数でも指定できます。
$ export UV_PUBLISH_USERNAME=admin $ export UV_PUBLISH_PASSWORD=admin123
こうすると、認証情報を求められなくなります。
$ uv publish --index mypypi Publishing 2 files http://192.168.33.11:8081/repository/pypi-hosted/ Uploading clover_scrape-0.0.1-py3-none-any.whl (1.7KiB) Uploading clover_scrape-0.0.1.tar.gz (1.0KiB)
なんなら、--indexも環境変数で指定できます。
$ export UV_PUBLISH_INDEX=mypypi $ uv publish Publishing 2 files http://192.168.33.11:8081/repository/pypi-hosted/ Uploading clover_scrape-0.0.1-py3-none-any.whl (1.7KiB) Uploading clover_scrape-0.0.1.tar.gz (1.0KiB)
こうすると、なにも指定しなくてよくなりますね。
このあたりの環境変数についてはこちらに記載があります。
公開したパッケージを使ってみる
最後に公開したパッケージを使ってみましょう。
uvプロジェクトを作成。こちらはアプリケーション用でよいでしょう。
$ uv init --vcs none app $ cd app
pyproject.tomlには、以下のようにtool.uv.indexを追加します。
pyproject.toml
[project] name = "app" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [] [[tool.uv.index]] name = "mypypi" url = "http://192.168.33.11:8081/repository/pypi-group/simple/"
tool.uv.indexにはnameとurlがあればOKです。パッケージを公開するわけではないのでpublish-urlは不要です。
パッケージインデックス、というものですね。末尾は/simpleというパスになります。
ドキュメントには出てこないものの、使っていない設定があります。気になるところを挙げておきましょう。
default = trueとすると、これはデフォルトインデックスを表します。デフォルトのデフォルトインデックスはPyPiです。
つまり、default = trueとするとPyPiを指定したリポジトリーで置き換えることができます。
explicit = trueは、明示的に指定した場合のみ使われるインデックスを表します。
たとえば以下はPyTorchのみで使われるインデックスを定義したことになるようです。
[tool.uv.sources]
torch = { index = "pytorch" }
[[tool.uv.index]]
name = "pytorch"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
話を戻しましょう。
では、公開したライブラリーを追加。
$ uv add clover-scrape
依存関係を含めて解決できました。
$ uv add clover-scrape Using CPython 3.12.3 interpreter at: /usr/bin/python3.12 Creating virtual environment at: .venv Resolved 7 packages in 162ms Prepared 6 packages in 19ms Installed 6 packages in 4ms + certifi==2025.8.3 + charset-normalizer==3.4.2 + clover-scrape==0.0.1 + idna==3.10 + requests==2.32.4 + urllib3==2.5.0
あとはライブラリーを使うコードを作成。
main.py
from clover_scrape import get_title def main(): print(get_title()) if __name__ == "__main__": main()
実行。
$ uv run main.py CLOVER🍀
OKですね。
おわりに
Sonatype Nexus 3でPyPiリポジトリーを作成して、uvでパッケージを公開してみました。
Pythonのパッケージ管理まわりを全然知らなかったので、そこから見ることになりましたが、かなり大変でした。
とはいえ、こうでもしないと見ないと思うのでいい機会になったということで…。
uv以外でパッケージをビルドして公開したり、純粋なPythonコード以外が入ってきたりするとだいぶ話が変わってくると
思うのですが、まずは基本ということで。