Chapter 09Lesson 02185–245 min

PyPI, Conda, Python Package Indexes, Proxy Behavior, Uploads, and Metadata Management: Guided Hands-On Workflow and Core Operations

Build a disposable PyPI hosted/proxy/group workflow, isolate pip and Twine state, proxy a harmless public wheel, publish a synthetic wheel and sdist, inspect Simple API metadata and hashes, consume from a clean environment, and compare Conda channel metadata safely.

PyPI hosted/proxy/groupTwinepip isolationPEP 691Hashes

Learning objectives

  • Create disposable PyPI hosted/proxy/group repositories and explain every state store they touch.
  • Use a temporary HOME, pip configuration, cache, and virtual environment so the lab cannot alter normal Python client state.
  • Proxy a harmless public wheel, build and publish a synthetic wheel/sdist, and consume it through a group.
  • Inspect HTML/JSON Simple API metadata and independently calculate SHA-256 hashes.
  • Model Conda channel metadata without requiring a paid product or a system-wide Conda installation.

Lab scope. Use only a disposable self-hosted Nexus Repository Community Edition 3.95.2 instance on loopback. The commands below are POSIX/Bash. On Windows PowerShell, use py -m venv, .venv\Scripts\Activate.ps1, $env:PIP_CONFIG_FILE, $env:TWINE_USERNAME/$env:TWINE_PASSWORD, and Get-FileHash -Algorithm SHA256. Do not reuse your normal pip.ini, .pypirc, or employer credentials.

1. Preflight: record versions and isolate all client state

Start with observation. This lab intentionally separates five locations: Nexus repository configuration, Nexus database metadata, Nexus blob bytes, Python client configuration/cache, and the local virtual environment. A failure in one should not be “fixed” by deleting another.

export NX_URL="http://127.0.0.1:8081"
export LAB="${TMPDIR:-/tmp}/nexus-ch09"
rm -rf "$LAB"
mkdir -p "$LAB/home" "$LAB/cache" "$LAB/project" "$LAB/evidence" "$LAB/downloads"
chmod 700 "$LAB" "$LAB/home"

python --version | tee "$LAB/evidence/python-version.txt"
python -m pip --version | tee "$LAB/evidence/pip-version.txt"
curl -fsS "$NX_URL/service/rest/v1/status" | tee "$LAB/evidence/nexus-status.txt"

python -m venv "$LAB/tooling-venv"
. "$LAB/tooling-venv/bin/activate"
export PIP_CACHE_DIR="$LAB/cache"
export PIP_CONFIG_FILE="$LAB/home/pip.conf"

Do not point PIP_CONFIG_FILE at your normal user configuration. This entire directory is disposable.

2. Create three disposable PyPI repositories

Using the Nexus UI under Settings → Repository → Repositories, create:

Repository Recipe Key settings
academy-ch09-hosted pypi (hosted) Blob store default; Disable redeploy; online.
academy-ch09-proxy pypi (proxy) Remote storage https://pypi.org/; blob store default; default cache settings.
academy-ch09-group pypi (group) Members: hosted first, proxy second; blob store default.

Hosted owns internal publication. Proxy mediates PyPI. Group is the only read index used by this lab. The client never receives a direct pypi.org index URL.

Create a dedicated local Nexus user such as academy-ch09-publisher with only read/browse on the group/proxy and add/read/browse on the hosted lab repository. Repository creation remains an administrator action; package publication should not.

3. Create temporary read credentials without placing the password in URLs

pip and curl can use netrc through their HTTP stack. Place the lab-only credential in the temporary HOME, restrict its permissions, and never display the file after creation. Twine does not rely on netrc in Sonatype's documented flow, so the upload will use temporary environment variables later.

read -r -p 'Disposable Nexus username: ' NX_USER
read -r -s -p 'Disposable Nexus password: ' NX_PASS; echo

cat > "$LAB/home/.netrc" <<EOF
machine 127.0.0.1
login $NX_USER
password $NX_PASS
EOF
chmod 600 "$LAB/home/.netrc"

cat > "$PIP_CONFIG_FILE" <<'EOF'
[global]
index-url = http://127.0.0.1:8081/repository/academy-ch09-group/simple
trusted-host = 127.0.0.1
disable-pip-version-check = true
EOF
chmod 600 "$PIP_CONFIG_FILE"

# Preserve the password in-shell only until the Twine upload; do not echo it.
export HOME="$LAB/home"

Because the URL contains no credential, it is safe to record the route itself as evidence. Do not run diagnostics that print the netrc file.

4. Prove proxy behavior with one harmless public wheel

Use packaging==25.0 as a small, known public package. Download only a binary wheel and do not install it. The first request should populate Nexus proxy metadata/blob state; a later fresh client download can be served from Nexus cache according to proxy freshness rules.

python -m pip download   --no-deps --only-binary=:all:   packaging==25.0   -d "$LAB/downloads/public"   -v 2>&1 | tee "$LAB/evidence/public-first-download.txt"

python - <<'PYI'
from pathlib import Path
import hashlib, os
for p in Path(os.environ["LAB"], "downloads", "public").glob("*.whl"):
    print(p.name, hashlib.sha256(p.read_bytes()).hexdigest())
PYI

Inspect academy-ch09-proxy in Browse/Search afterward. The bytes belong to proxy cache state, not hosted internal publication.

5. Install build tooling through the controlled group

The synthetic package uses current PyPA tooling. Install build and twine only inside the disposable tooling virtual environment, through the Nexus group. This intentionally exercises proxying; it is not a recommendation that every production build dynamically downloads its build tooling.

python -m pip install --upgrade build twine   2>&1 | tee "$LAB/evidence/tooling-install.txt"

python -m build --version | tee "$LAB/evidence/build-version.txt"
python -m twine --version | tee "$LAB/evidence/twine-version.txt"

6. Build a tiny synthetic project

The distribution project is learner-ch09-widget; its import package is learner_ch09_widget. It has no dependencies, no network activity, and no build-time custom code. The build creates one wheel and one sdist from the same source tree.

[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "learner-ch09-widget"
version = "0.1.0"
description = "Disposable Nexus PyPI training package"
requires-python = ">=3.10"
cd "$LAB/project"
mkdir -p src/learner_ch09_widget
cat > pyproject.toml <<'EOF'
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "learner-ch09-widget"
version = "0.1.0"
description = "Disposable Nexus PyPI training package"
requires-python = ">=3.10"
EOF
cat > src/learner_ch09_widget/__init__.py <<'EOF'
__version__ = "0.1.0"

def identity():
    return "learner-ch09-widget/0.1.0"
EOF

python -m build 2>&1 | tee "$LAB/evidence/build-package.txt"
python -m twine check dist/* | tee "$LAB/evidence/twine-check.txt"
ls -lh dist/ | tee "$LAB/evidence/dist-files.txt"

python -m build may resolve build-system requirements in an isolated environment. Because the client is configured to the Nexus group, those dependency requests remain behind the controlled index route.

7. Capture local hashes before publication

python - <<'PYI' | tee "$LAB/evidence/local-sha256.txt"
from pathlib import Path
import hashlib
for p in sorted(Path("dist").iterdir()):
    if p.is_file():
        print(hashlib.sha256(p.read_bytes()).hexdigest(), p.name)
PYI

This is the producer-side evidence. After publication and retrieval, the same file should hash identically if promotion/transport preserved bytes.

8. Publish only to the hosted PyPI repository

Twine's repository URL for Nexus points to the hosted repository root, not its /simple consumer endpoint. Keep the password out of the command line and out of .pypirc by exporting it only for the upload process, then unsetting it.

export TWINE_REPOSITORY_URL="$NX_URL/repository/academy-ch09-hosted/"
export TWINE_USERNAME="$NX_USER"
export TWINE_PASSWORD="$NX_PASS"

python -m twine upload dist/*   2>&1 | tee "$LAB/evidence/twine-upload.txt"

unset TWINE_PASSWORD TWINE_USERNAME TWINE_REPOSITORY_URL NX_PASS

Expected state: Nexus creates/updates PyPI component and asset metadata in its database and stores the wheel/sdist bytes in the hosted repository's blob store. No proxy state is required for these internal files.

9. Inspect the published project through HTML and JSON Simple API

Use the group endpoint because that is the consumer contract. The hosted-first member order should expose the internal project. Capture both HTML and JSON. Modern JSON responses can include hashes, sizes, upload times, versions, and core-metadata information depending on the file and API representation.

PROJECT_URL="$NX_URL/repository/academy-ch09-group/simple/learner-ch09-widget/"

curl -fsS --netrc-file "$LAB/home/.netrc"   "$PROJECT_URL"   | tee "$LAB/evidence/simple-project.html"

curl -fsS --netrc-file "$LAB/home/.netrc"   -H 'Accept: application/vnd.pypi.simple.v1+json'   "$PROJECT_URL"   | tee "$LAB/evidence/simple-project.json"

Do not confuse the project index document with the wheel itself. The index is metadata that points to distribution files.

10. Consume from a fresh virtual environment and fresh cache

Create a second virtual environment and separate cache. This proves the group can serve the package without relying on the producer environment's installed files or wheel cache.

python -m venv "$LAB/consumer-venv"
. "$LAB/consumer-venv/bin/activate"
export PIP_CACHE_DIR="$LAB/consumer-cache"
mkdir -p "$PIP_CACHE_DIR"
export HOME="$LAB/home"
export PIP_CONFIG_FILE="$LAB/home/pip.conf"

python -m pip install --no-deps --no-cache-dir learner-ch09-widget==0.1.0   -v 2>&1 | tee "$LAB/evidence/internal-install.txt"

python - <<'PYI' | tee "$LAB/evidence/import-proof.txt"
import learner_ch09_widget as p
print(p.__version__)
print(p.identity())
PYI

11. Re-download and compare SHA-256

mkdir -p "$LAB/downloads/internal"
python -m pip download --no-deps --only-binary=:all: --no-cache-dir   learner-ch09-widget==0.1.0   -d "$LAB/downloads/internal"   2>&1 | tee "$LAB/evidence/internal-download.txt"

python - <<'PYI' | tee "$LAB/evidence/retrieved-sha256.txt"
from pathlib import Path
import hashlib, os
for p in Path(os.environ["LAB"], "downloads", "internal").glob("*.whl"):
    print(hashlib.sha256(p.read_bytes()).hexdigest(), p.name)
PYI

Compare this digest with the producer-side wheel hash and the hash exposed in Simple API metadata. Equality proves exact byte identity across publication and retrieval; it still does not prove vulnerability safety or provenance.

12. Conda behavior: use a faithful metadata fixture without requiring Conda

Conda resolution starts from a channel and platform subdirectory rather than a PyPI Simple project page. The following fixture is intentionally local and synthetic; it demonstrates the relationship among repodata.json, package filename, build string, dependencies, and SHA-256 without downloading or executing a Conda package.

{
  "info": {"subdir": "noarch"},
  "packages.conda": {
    "learner-ch09-conda-0.1.0-py_0.conda": {
      "name": "learner-ch09-conda",
      "version": "0.1.0",
      "build": "py_0",
      "build_number": 0,
      "subdir": "noarch",
      "depends": ["python >=3.10"],
      "sha256": "<fixture-sha256>",
      "size": 1234
    }
  }
}

On Nexus 3.95.2, a live optional exercise may create conda (proxy), conda (hosted), and conda (group) repositories. Hosted/group support is not a Pro requirement; it has been available since 3.92.0. If you do not already use Conda, the fixture is the mandatory path—do not install a large ecosystem merely to prove this chapter's PyPI concepts.

13. Small challenge: choose the correct control

Your CI must publish learner-ch09-widget, while developer machines should read internal and public Python packages from one managed endpoint. Which URLs should each client receive?

The publisher should receive the hosted repository upload URL. Consumers should receive the group /simple URL. They should not receive pypi.org/simple as an extra-index-url. The group can contain the public proxy, keeping external traffic behind Nexus.

14. Cleanup

Do not remove repositories yet if you will continue to Lesson 4's failure drills. To end the lab now, remove the group first, then proxy and hosted through Nexus UI/API; remove the disposable publisher user; preserve only redacted evidence. Then delete the local $LAB directory after confirming it contains no evidence you intend to retain. Never delete Nexus blob files or database rows manually.

Knowledge check

Why does Twine upload to hosted rather than the group /simple URL?

What changed in Nexus when the wheel and sdist were uploaded?

Why use a second venv and cache for consumption?

What does matching local, Simple API, and retrieved SHA-256 prove?

Why is the Conda fixture valid mandatory learning?

15. Summary and next step

You have traced both public proxy resolution and private publication through one controlled Nexus topology, proven the internal bytes independently, and kept Python client state isolated. Lesson 3 turns those mechanics into design choices around namespace safety, index routing, immutable publication, wheel/sdist policy, and PyPI-versus-Conda boundaries.

Official references and version notes

Version-sensitive statements were rechecked against Sonatype and Python Packaging primary documentation on 2026-08-26. The mandatory lab assumes self-hosted Nexus Repository Community Edition 3.95.2, the bundled/supported Java 21 runtime, a disposable single-node local instance, and a current Python 3 environment. Record python --version, python -m pip --version, python -m twine --version, and python -m build --version locally because Python packaging clients evolve independently of Nexus.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.