Skip to content
Select theme
  • Auto
  • Dark
  • Light

Python with pip, uv and Poetry

An admin first makes a repository with the format Python (PyPI) (see Run the server). The examples use the server repo.example.com and a repository named python, so the index is this:

https://repo.example.com/pypi/python/simple/

Put your own values in the blocks below. The Use this repository tab of the repository shows the same blocks with your values.

  1. Make a read token.

    Anyone can download

    Skip this step. Anyone can download.

    Only signed-in people

    Settings › Tokens › New read token. Type a name, then click Create token.

  2. Point the tool at the repository.

    pip, anyone can download

    ~/.config/pip/pip.conf
    [global]
    index-url = https://repo.example.com/pypi/python/simple/

    On Windows, the file is %APPDATA%\pip\pip.ini.

    pip, only signed-in people

    ~/.config/pip/pip.conf
    [global]
    index-url = https://YOUR_USERNAME:YOUR_READ_TOKEN@repo.example.com/pypi/python/simple/

    pip reads the username and the token from the address. On Windows, the file is %APPDATA%\pip\pip.ini. The file holds your token, so keep it in your home folder.

    uv

    pyproject.toml
    [[tool.uv.index]]
    name = "python"
    url = "https://repo.example.com/pypi/python/simple/"
    default = true

    uv, only signed-in people

    Then give uv your username and the token. Paste the token at the hidden prompt.

    Terminal window
    printf 'Read token: '; read -rs READ_TOKEN; echo
    export UV_INDEX_PYTHON_USERNAME=YOUR_USERNAME
    export UV_INDEX_PYTHON_PASSWORD="$READ_TOKEN"

    The variables name the index in capitals, with _ for every character other than a letter or a digit. In CI, set them as secrets.

    Poetry, anyone can download

    Terminal window
    poetry source add --priority=primary python https://repo.example.com/pypi/python/simple/

    Poetry, only signed-in people

    Paste the token at the hidden prompt.

    Terminal window
    poetry source add --priority=primary python https://repo.example.com/pypi/python/simple/
    printf 'Read token: '; read -rs READ_TOKEN; echo
    poetry config http-basic.python YOUR_USERNAME "$READ_TOKEN"
  3. Install a package.

    pip

    Terminal window
    pip install "acme-billing==1.2.0"

    uv

    Terminal window
    uv add "acme-billing==1.2.0"

    Poetry

    Terminal window
    poetry add "acme-billing==1.2.0"

Done when the tool installs the package with no error.

Your own packages

A hosted repository holds your own packages only. pip then cannot install their dependencies from PyPI through it. To install both through one index, ask your server admin for a group of your repository and a proxy of PyPI. Then point the tool at the group.

Public packages too

A group asks its members in order. The first member that has a project name owns it, and the group serves every file of that project from that member only. So a package on PyPI with the name of your private package cannot take its place, even with a higher version. Your admin puts your hosted repository before the proxy.

You set up the project once. After that, everyone on the project publishes with one command.

  1. Open the repository in the web UI, then the Publish to this repository tab. Click Set up a project, and confirm with your second factor.

  2. Paste the setup line in a terminal, in the folder that holds pyproject.toml or setup.py. The CLI writes telesphorepo.toml. Commit it. See Set up a project for the CI file.

  3. Publish.

    Terminal window
    telesphorepo publish

Done when the CLI prints Uploaded for each file, for example Uploaded acme_billing-1.2.0-py3-none-any.whl (acme-billing 1.2.0) to python.

The CLI builds the project into a temporary folder, then uploads every wheel and sdist of the build:

Project The CLI builds with
[tool.poetry] in pyproject.toml poetry build
a uv.lock uv build
anything else python -m build. Install it with python -m pip install build

To upload files that you built, name them: telesphorepo publish dist/*.whl. The CLI then builds nothing.

A file that is on the server already, with other bytes, answers 409 File already exists. Raise the version.

Publish with twine, uv or Poetry

These tools upload with a publish token. Make one in Settings › Tokens › New publish token: choose the repository, then confirm with your second factor. The token works for 15 minutes. The username is __token__, and the upload address is https://repo.example.com/pypi/python/.

twine asks for the token at its password prompt:

Terminal window
twine upload --repository-url https://repo.example.com/pypi/python/ -u __token__ dist/*

uv reads the token from the environment:

Terminal window
printf 'Publish token: '; read -rs UV_PUBLISH_TOKEN; echo
export UV_PUBLISH_TOKEN
uv publish --publish-url https://repo.example.com/pypi/python/

Poetry reads it from the environment too:

Terminal window
poetry config repositories.telesphorepo https://repo.example.com/pypi/python/
printf 'Publish token: '; read -rs POETRY_HTTP_BASIC_TELESPHOREPO_PASSWORD; echo
export POETRY_HTTP_BASIC_TELESPHOREPO_USERNAME=__token__ POETRY_HTTP_BASIC_TELESPHOREPO_PASSWORD
poetry publish --build -r telesphorepo

A password or a read token cannot upload.

The CLI uses the identity token of the job, so the job stores no secret. An admin adds a CI pipeline for the job first, as in Run the server. When you choose GitHub Actions at setup, the CLI writes the file for the build tool of the project:

GitHub Actions with pip

.github/workflows/publish.yml
on:
push:
branches: ["main"]
permissions:
id-token: write
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: 3.x
- run: python -m pip install build
- run: curl -fsSL https://repo.example.com/install.sh | sh
- run: telesphorepo publish

GitHub Actions with uv

.github/workflows/publish.yml
on:
push:
branches: ["main"]
permissions:
id-token: write
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- run: curl -fsSL https://repo.example.com/install.sh | sh
- run: telesphorepo publish

GitHub Actions with Poetry

.github/workflows/publish.yml
on:
push:
branches: ["main"]
permissions:
id-token: write
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: 3.x
- run: pipx install poetry
- run: curl -fsSL https://repo.example.com/install.sh | sh
- run: telesphorepo publish

For Jenkins, see Set up a project.

Find your problem, then do the fix

pip ignores an http:// index. pip uses an http:// index only on localhost, or when its host is trusted. Add trusted-host = repo.example.com under [global] in pip.conf. For real use, serve the server over HTTPS. See Run in production.

pip cannot find a dependency, such as requests. The repository is hosted, and holds your own packages only. Ask your server admin for a group with a proxy of PyPI.

401 when you install. The repository is for signed-in people only. Check the username and the read token. For uv, the variables must name the index in capitals: UV_INDEX_PYTHON_USERNAME for the index python.

A group serves your private package, not the public one with the same name. The first member that has a project owns it. Rename your project, or ask your admin to change the order of the members.

409 File already exists. A file with that name is on the server with other bytes. Raise the version. The same bytes again are fine.

400 when you upload. The name and the version in the file name must match the metadata inside the file. Build the file again with your build tool.

The CLI says that poetry, uv or python is not on the PATH. The CLI builds with the tool of the project. Install it, or build yourself and run telesphorepo publish dist/*.

twine prints only the status line. Add --verbose. twine then prints the reason that the server gives.