Skip to content

uv Repositories

View as Markdown

uv installs from a CloudRepo Python repository and publishes to it. CloudRepo serves the same simple index and the same upload API that pip and twine use, so uv needs the repository’s address and a credential.

1. Put the credential in ~/.netrc. uv reads it for the host on the machine line. Keep the file readable by you alone (chmod 600 ~/.netrc):

~/.netrc
machine your-org.mycloudrepo.io
login you@example.com
password YOUR_REPOSITORY_TOKEN

2. Build and publish. You need a project that builds a wheel or a source distribution (see uv’s building and publishing guide). Give uv publish its credential in two environment variables rather than as options, which would put the token on the command line. Set CLOUDREPO_USERNAME and CLOUDREPO_TOKEN from your secret store, and give uv the repository URL without /simple/:

Terminal
uv build
UV_PUBLISH_USERNAME="$CLOUDREPO_USERNAME" UV_PUBLISH_PASSWORD="$CLOUDREPO_TOKEN" \
uv publish --publish-url https://your-org.mycloudrepo.io/repositories/your-repo dist/*

Expected: uv prints Uploading and the file name for each file in dist/, and exits without an error. The files are then in the repository, and the admin portal lists them.

3. Install. uv pip install takes the index URL with /simple/. With --index-url CloudRepo is the only index for that command, which is right for a package that has no public dependencies. uv finds the credential in ~/.netrc, so the URL carries none:

Terminal
uv venv
uv pip install --index-url https://your-org.mycloudrepo.io/repositories/your-repo/simple/ docs-uv==1.0.0

Expected: uv prints Installed 1 package and + docs-uv==1.0.0.

In a project, name CloudRepo as an index that serves only the packages you pin to it. An index with explicit = true is asked for nothing else, so every other package still comes from PyPI.

Pin every private package before you run uv lock or uv sync, including each private package that your private packages need. uv applies a [tool.uv.sources] entry only to a package that dependencies or a dependency group lists. It ignores an entry for any other name, and it prints no warning. It looks that name up on PyPI instead, where anyone can publish a package under your private package’s name, and uv locks and installs it. List each private package in dependencies and in [tool.uv.sources].

When a private package starts to need another private package, add that one to dependencies and to [tool.uv.sources] too before you run uv lock --upgrade. Otherwise uv takes it from PyPI.

The example is a project that depends on the package you just published:

pyproject.toml
[project]
name = "my-service"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["docs-uv"]
[[tool.uv.index]]
name = "cloudrepo"
url = "https://your-org.mycloudrepo.io/repositories/your-repo/simple/"
explicit = true
[tool.uv.sources]
docs-uv = { index = "cloudrepo" }

Lock and install. uv finds the credential in the same ~/.netrc:

Terminal
uv lock
uv sync --locked

Expected: uv lock writes uv.lock, which records CloudRepo as the source of docs-uv. uv sync --locked checks the environment against that lock file. If docs-uv is already in .venv, as after the uv pip install earlier on this page, it prints Checked 1 package. In an environment that does not hold it yet, it installs the package and prints + docs-uv==1.0.0.

Three more things to get right:

  • Do not make PyPI an extra index beside CloudRepo. uv asks an --extra-index-url index before the default index, so with CloudRepo as --index-url and PyPI as the extra index, a package someone publishes on PyPI under your private package’s name is installed instead of yours.
  • Where a file is awkward, such as CI, set UV_INDEX_CLOUDREPO_USERNAME and UV_INDEX_CLOUDREPO_PASSWORD from your secret store. uv reads UV_INDEX_<NAME>_USERNAME and UV_INDEX_<NAME>_PASSWORD for the index called <NAME>, written in upper case. UV_INDEX_USERNAME and UV_INDEX_PASSWORD are not uv settings: uv sends no credential for them and the repository answers 401.
  • Publish with --publish-url, as above. uv publish --index cloudrepo also reads the index, and CloudRepo’s index lists no file hashes, so uv stops with Hash is missing in index for a file that is already there.
  • 401. uv prints could not be queried due to a lack of valid authentication credentials (401 Unauthorized), and on a publish Server returned status code 401 Unauthorized. The credential was refused or never sent. The username must be the email address of the account that created the token. __token__ is refused: that is the convention on pypi.org, and CloudRepo does not use it. The password must be the token itself, and a token that is expired or revoked fails the same way: the Repository Tokens page in the admin portal shows its status. Check that the machine line of ~/.netrc is exactly the host in the URL, and that the variable names are the ones above.
  • 403. uv prints returned a 403 Forbidden error, and on a publish Server returned status code 403 Forbidden. The token does not reach this repository, or it is Read only and you published. A token reaches only the repositories ticked when it was created.
  • 404. uv prints was not found in the package registry, naming the package. The repository does not hold that name, or the URL is wrong: check the organization and repository names in it. A proxy repository that the token does not reach also answers 404, not 403.
  • 409, on publish. The repository has Overwrite Protection on, and a file at that path already exists. uv prints Server returned status code 409 Conflict and, after Server says:, the explanation CloudRepo sends with it. Upload a new version, or turn Overwrite Protection off for that repository: see Python Repositories.

More: Repository tokens, for every client’s credential; Python Repositories, for pip and twine and the Python repository settings; Install Python packages from CloudRepo, for installing with pip.