DEV Community

Wen Ping
Wen Ping

Posted on Originally published at anguskit.com

How to use a private PyPI index: from install to twine upload and pip install

twine upload returns 401, or pip install says No matching distribution found—usually the package is fine and the private index is not wired: service down, upload URL confused with the install index, username not __token__, or a missing /simple/ suffix.

This article walks through AngusRepo Community self-hosted in order: install → repos and token → .pypirc / pip.conf → minimal-package upload and install → verify. Examples use repo.example.com—swap your domain.

1. Install (get a working console)

Prerequisites: Docker Engine + Compose v2; about 2 CPU / 4 GB; open 80; resolve gm.example.com and repo.example.com (labs can use /etc/hosts). Full steps: First run.

curl --fail --location --progress-bar -o AngusRepo-Community-1.0.0.zip \
  https://repo.anguskit.com/raw/raw-public/AngusKit/repo/AngusRepo-Community-1.0.0.zip
unzip AngusRepo-Community-1.0.0.zip
cd AngusRepo-1.0.0
./install.sh
Enter fullscreen mode Exit fullscreen mode

Wizard aligned with quickstart: Compose; Access → 2; TLS → 4 (HTTP :80); Database → Compose MySQL; Public DNS name → example.com. After Install finished.:

./bin/angusctl.sh doctor   # wait for doctor: OK
Enter fullscreen mode Exit fullscreen mode

Open http://gm.example.com/, then http://repo.example.com/. For HTTPS see Install & deploy.

The rest assumes HTTP self-hosted. On HTTP, pip usually needs trusted-host; after real TLS, switch to https:// and drop it. Create repos and a token before client config.

2. Repos, token, and topology

twine upload only goes to HOSTED; PROXY and GROUP are read-only. Create:

Example name Type Role
pypi-hosted HOSTED twine upload target; disable overwrite
pypi-proxy PROXY Proxy PyPI (https://pypi.org)
pypi-all GROUP Unified pip entry; hosted → proxy

Keep the two paths distinct:

  • Upload: /pypi/<repo>/ (HOSTED root)
  • Install: /pypi/<repo>/simple/ (PEP 503; trailing /simple/ required)

Also prepare:

  • [ ] Access token (shown once—save it)
  • [ ] CI Write on pypi-hosted; developers Read on pypi-all
  • [ ] GROUP order hosted → pypi-proxy
export REPO_TOKEN="your_access_token"
export REPO_HOST="repo.example.com"
Enter fullscreen mode Exit fullscreen mode

Field detail: PyPI Format.

3. Install: pip.conf / index-url

Default: install via GROUP (pypi-all/simple/), upload via HOSTED.

[global]
index-url = http://repo.example.com/pypi/pypi-all/simple/
trusted-host = repo.example.com
Enter fullscreen mode Exit fullscreen mode

One-shot:

pip install some-pkg \
  --index-url "http://$REPO_HOST/pypi/pypi-all/simple/" \
  --trusted-host "$REPO_HOST"
Enter fullscreen mode Exit fullscreen mode

Internal-only with public still on PyPI:

pip install your-package \
  --index-url https://pypi.org/simple \
  --extra-index-url "http://$REPO_HOST/pypi/pypi-hosted/simple/" \
  --trusted-host "$REPO_HOST"
Enter fullscreen mode Exit fullscreen mode

index-url / extra-index-url must end with /simple/.

4. Publish: .pypirc and twine

username is always __token__; put the access token in password.

[distutils]
index-servers =
  angusrepo

[angusrepo]
repository = http://repo.example.com/pypi/pypi-hosted/
username = __token__
password = your_access_token
Enter fullscreen mode Exit fullscreen mode
python3 -m pip install build twine
python3 -m build
twine upload \
  --repository-url "http://$REPO_HOST/pypi/pypi-hosted/" \
  --username __token__ \
  --password "$REPO_TOKEN" \
  dist/*
Enter fullscreen mode Exit fullscreen mode

Or twine upload -r angusrepo dist/*. Same-version re-upload should fail—bump the version.

5. Verify the loop (minimal package)

mkdir -p /tmp/angus-pypi-demo && cd /tmp/angus-pypi-demo
cat > pyproject.toml <<'EOF'
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[project]
name = "angus-demo-utils"
version = "1.0.0"
description = "AngusRepo PyPI smoke package"
requires-python = ">=3.9"
EOF
mkdir -p angus_demo_utils
echo 'OK = True' > angus_demo_utils/__init__.py

python3 -m pip install -q build twine
python3 -m build
twine upload \
  --repository-url "http://$REPO_HOST/pypi/pypi-hosted/" \
  --username __token__ \
  --password "$REPO_TOKEN" \
  dist/*
Enter fullscreen mode Exit fullscreen mode

Clean install:

python3 -m venv /tmp/angus-pypi-venv && source /tmp/angus-pypi-venv/bin/activate
pip install angus-demo-utils==1.0.0 \
  --index-url "http://$REPO_HOST/pypi/pypi-all/simple/" \
  --trusted-host "$REPO_HOST"
python -c "import angus_demo_utils; print(angus_demo_utils.OK)"
Enter fullscreen mode Exit fullscreen mode

Expect True. The console should list 1.0.0.

Minimal CI snippet

- run: |
    python -m pip install build twine
    python -m build
    twine upload \
      --repository-url "http://$REPO_HOST/pypi/pypi-hosted/" \
      --username __token__ \
      --password "$REPO_TOKEN" \
      dist/*
Enter fullscreen mode Exit fullscreen mode

Poetry: point the primary source at GROUP /simple/ with username __token__. Pipelines: Jenkins build, test, publish.

6. Day-two ops

Cleanup — keep last N on HOSTED; Dry Run first—Cleanup Policies.

Cold PROXY — first public pulls are slow; warm if needed.

Ingest checks — light gate in Repo; deeper scanning elsewhere—admission gates and SBOM.

Error cheat sheet

Symptom Likely cause Fix
twine 401 username not __token__, or bad token Fix credentials; check HOSTED Write
No matching distribution found missing /simple/, wrong repo use …/pypi-all/simple/
Upload OK, install fails still hitting public PyPI only fix index-url to GROUP
SSL errors on HTTP no trusted-host add host in pip.conf
409 / file exists same version re-upload bump version

FAQ

Can I twine to a GROUP?

No. Upload only to HOSTED; pip install via GROUP /simple/.

Can username be my login name?

Per the PyPI token convention, use __token__. A login name usually yields 401.

Is trusted-host required on HTTP?

Yes. Without TLS, pip still expects certificate checks unless you trust the host.

Takeaways

  • Install console → HOSTED + PROXY + GROUP → token.
  • Upload HOSTED root; install GROUP /simple/.
  • __token__ in username; smoke-test build → twine → pip.

Reference articles

Top comments (0)