# uv Repositories

> Use a CloudRepo Python repository with uv: a credential in ~/.netrc, publishing with uv publish, installing, pinning private packages to CloudRepo in a project, and what a 401, 403, 404 or 409 means.

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.

## Before you start

- A Python repository. If you have none, see [Creating a Repository](/docs/manage/repositories.html#creating-a-repository).
- A repository token that reaches it, with **Read + write** if you will publish. See [Repository Tokens: Create One and Authenticate](/docs/authenticate/repository-tokens.html). The username is the email address of the account that created the token, and the password is the token.
- [uv](https://docs.astral.sh/uv/getting-started/installation/), installed from uv’s own instructions.

> **Note:** Use a repository token, not a password. An organization owner’s portal password is not accepted for uv, pip or twine. A token the owner creates works like anyone else’s.

## Publish and install

**uv**

**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`

```ini
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](https://docs.astral.sh/uv/guides/package/)). 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**

```bash
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](https://admin.cloudrepo.io) 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**

```bash
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`.

## Pin private packages in a project

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:

**uv**

`pyproject.toml`

```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**

```bash
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.

> **Note:** Each file you publish to a Python repository, a wheel or a source distribution, can be up to 5 GiB (5,368,709,120 bytes). A larger upload is refused with `413`.

## When uv answers 401, 403, 404 or 409

- **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](/docs/formats/python.html#overwrite-protection).

More: [Repository tokens](/docs/authenticate/repository-tokens.html), for every client’s credential; [Python Repositories](/docs/formats/python.html), for pip and twine and the Python repository settings; [Install Python packages from CloudRepo](/docs/consume/python.html), for installing with pip.

---

The page: https://www.cloudrepo.io/docs/formats/uv.html
