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 

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 

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" 

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 

One-shot:

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

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" 

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

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

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)" 

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

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