以下の内容はhttps://kazuhira-r.hatenablog.com/entry/2025/08/03/200320より取得しました。


Sonatype Nexus 3でPyPiリポジトリーを作成して、uvでパッケージを公開する

これは、なにをしたくて書いたもの?

Sonatype Nexus 3でPyPiリポジトリーを作成して、Pythonパッケージを公開してみます。

Pythonパッケージと配布まわり

Pythonのパッケージと配布に関する知識がないので、まずはこちらを見ていこうと思います。

Pythonパッケージング

Pythonのパッケージングについては、こちらのドキュメントを見るのがよさそうです。

Overview of Python Packaging - Python Packaging User Guide

以下の2種類があるようです。

sdistとwheelは、両方とも公開することが望ましいされています。wheelのソースコードがsdistなので、利用者の環境に対応する
wheelがなくてもsdistからビルドできる可能性があるからです。またpipでパッケージをインストールする際に、sdistとwheelの
両方が存在する場合はデフォルトでwheelが優先されます。

各配布物の仕様はこちら。

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
  • パッケージ配信サービス(たとえば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リポジトリーに話が移せます。

PyPI Repositories

他のリポジトリーと同じように、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

このあたりも関係ありそうです。

ひとまず試してみましょうか。

環境

今回の環境はこちら。

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 NexusPyPiリポジトリーに公開し、別のuvプロジェクトで
使う、というものにします。

内容としては、このブログをスクレイピングするライブラリーにしましょう。

PyPiリポジトリーの作成

まずは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

では、PyPiリポジトリーを構築。

$ terraform init
$ terraform apply

これでPyPiリポジトリーの準備は完了です。

PyPiパッケージの作成と公開

次は、作成したPyPiリポジトリーに自前のパッケージを公開します。

まずはuvプロジェクトの作成。

$ uv init --vcs none --lib clover-scrape
$ uv init --vcs none clover-scrape
$ cd clover-scrape

uv initの時に、--libオプションを指定することでライブラリー用の構成になります。

Creating projects / Libraries

こんな内容ですね。

$ 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が設定されていることになります。

Build backend | 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.indexpublish-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)

こうすると、なにも指定しなくてよくなりますね。

このあたりの環境変数についてはこちらに記載があります。

Environment variables | uv

公開したパッケージを使ってみる

最後に公開したパッケージを使ってみましょう。

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にはnameurlがあればOKです。パッケージを公開するわけではないのでpublish-urlは不要です。

パッケージインデックス、というものですね。末尾は/simpleというパスになります。

Package indexes | uv

ドキュメントには出てこないものの、使っていない設定があります。気になるところを挙げておきましょう。

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コード以外が入ってきたりするとだいぶ話が変わってくると
思うのですが、まずは基本ということで。




以上の内容はhttps://kazuhira-r.hatenablog.com/entry/2025/08/03/200320より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14