Chapter 09Lesson 05210–275 min

Checkpoint Lab — PyPI, Conda, Python Package Indexes, Proxy Behavior, Uploads, and Metadata Management

Publish a tiny internal Python package to a disposable Nexus PyPI hosted repository, consume it from a clean virtual environment through an internal-only group, verify Simple API metadata and SHA-256 identity, prove the client has no direct public fallback, and clean up only disposable state.

Checkpoint labInternal-only indexSHA-256Fresh venvSafe cleanup

Checkpoint objectives

  • Create an internal-only PyPI publication/read path plus a separate Nexus public-proxy bootstrap path on a disposable instance.
  • Predict database/blob/metadata/authorization changes before publishing.
  • Build, upload, index, download, install, and hash one synthetic Python release.
  • Prove the client has no direct public index fallback for the internal namespace.
  • Produce a redacted evidence packet and remove the lab state through supported operations.

Checkpoint isolation. This lab uses new repository names so it does not depend on Lesson 2 state. Mandatory assumptions: Nexus Repository Community Edition 3.95.2, Java 21 on the Nexus side, loopback HTTP only, H2 acceptable only for this disposable single-node lab, default file blob store, current Python 3, and a dedicated least-privilege lab user. Production should use HTTPS and PostgreSQL as appropriate.

1. Scenario and success criteria

You operate an internal Python package named learner-ch09-checkpoint. It must be published to Nexus, discovered through a controlled index, installed into a fresh virtual environment, and verified by SHA-256. The client must not have a direct pypi.org fallback for this internal endpoint.

To make that requirement observable while keeping the lab reproducible, the checkpoint uses three repositories: an internal hosted repository, an internal-only group containing only that hosted member, and a separate public PyPI proxy used only to bootstrap build/twine/setuptools. The bootstrap proxy is never a member of the internal group and is never configured as extra-index-url. This cleanly separates tooling acquisition from internal-package candidate discovery.

2. Preflight and exact assumptions

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

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

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

The virtual environment is empty apart from its Python/pip bootstrap. Tooling will be installed through a dedicated Nexus proxy in section 5 before the client is switched to the internal-only route.

3. Create the internal-only topology plus a tooling proxy

In Nexus UI create:

Repository Recipe Configuration
academy-ch09cp-hosted pypi (hosted) Blob store default; online; Disable redeploy.
academy-ch09cp-public-proxy pypi (proxy) Remote URL https://pypi.org/; online; default cache values are acceptable for this disposable lab.
academy-ch09cp-internal pypi (group) Single member: academy-ch09cp-hosted; blob store default.

Create user academy-ch09cp-publisher with read/browse on the internal group and public proxy, plus add/read/browse on the hosted repository. Do not grant repository administration, delete, security administration, blob-store administration, or system privileges.

4. Predict state changes before you act

Prediction Before publication After publication
Hosted repository assets No learner-ch09-checkpoint wheel/sdist. Wheel and sdist assets exist under hosted ownership.
Blob store No bytes for those distribution files. Blob usage/count increases by the uploaded distribution content and metadata-related storage.
Database metadata No project/version component record. Project/version/assets are indexed as Nexus repository metadata.
Group index No project detail entry. Group /simple/learner-ch09-checkpoint/ exposes hosted files.
Public proxy Empty or unchanged before tooling bootstrap. May cache build/twine/setuptools dependencies during bootstrap, but internal package publication/install must not route through it.
Client cache Empty checkpoint cache. May contain HTTP/wheel cache entries after build-tool/install operations.

Write down at least two predictions in your evidence notes. After each operation, verify rather than merely assume they happened.

5. Bootstrap tooling through Nexus, then lock the client to the internal-only route

This is a deliberate two-phase client configuration. Phase A reaches the dedicated Nexus public proxy only long enough to install packaging tools. Phase B overwrites that configuration with the hosted-only internal group. There is never an extra-index-url, and pip never talks directly to pypi.org.

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"

# Phase A: bootstrap packaging tools through Nexus public proxy only.
cat > "$PIP_CONFIG_FILE" <<'EOF'
[global]
index-url = http://127.0.0.1:8081/repository/academy-ch09cp-public-proxy/simple
trusted-host = 127.0.0.1
disable-pip-version-check = true
EOF
chmod 600 "$PIP_CONFIG_FILE"

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

# Phase B: replace the route; internal package discovery is hosted-only.
cat > "$PIP_CONFIG_FILE" <<'EOF'
[global]
index-url = http://127.0.0.1:8081/repository/academy-ch09cp-internal/simple
trusted-host = 127.0.0.1
disable-pip-version-check = true
EOF
chmod 600 "$PIP_CONFIG_FILE"

# Verify the route without printing the credential file.
grep -E '^(index-url|trusted-host)' "$PIP_CONFIG_FILE"   | tee "$LAB/evidence/pip-route.txt"

if grep -qiE 'extra-index-url|pypi\.org|academy-ch09cp-public-proxy' "$PIP_CONFIG_FILE"; then
  echo 'FAIL: public/extra index remains in the internal client route' >&2
  exit 1
fi

The absence of extra-index-url and the absence of the tooling proxy from the final pip config are checkpoint evidence. The internal group itself contains only the hosted repository.

6. Build one harmless release

cd "$LAB/project"
mkdir -p src/learner_ch09_checkpoint
cat > pyproject.toml <<'EOF'
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "learner-ch09-checkpoint"
version = "1.0.0"
description = "Disposable internal-only Nexus checkpoint"
requires-python = ">=3.10"
EOF
cat > src/learner_ch09_checkpoint/__init__.py <<'EOF'
__version__ = "1.0.0"

def evidence():
    return "learner-ch09-checkpoint/1.0.0"
EOF

python -m build --no-isolation 2>&1 | tee "$LAB/evidence/build.txt"
python -m twine check dist/* | tee "$LAB/evidence/twine-check.txt"

--no-isolation is intentional here: the build requirements were installed during the Nexus-proxy bootstrap phase, so this build does not attempt to discover setuptools through the internal-only package index. Do not “fix” an internal build by adding public extra-index-url.

7. Record producer-side hashes

python - <<'PYI' | tee "$LAB/evidence/producer-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

8. Publish to hosted with a narrow credential

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

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

unset TWINE_PASSWORD TWINE_USERNAME TWINE_REPOSITORY_URL NX_PASS

Immediately inspect Nexus Browse/Search. Record the component/project/version and the two distribution assets. Do not edit the blob store to “verify” storage; use supported UI/API/storage metrics.

9. Prove Simple API metadata from the internal-only group

PROJECT_URL="$NX_URL/repository/academy-ch09cp-internal/simple/learner-ch09-checkpoint/"

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

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

Expected JSON evidence includes file entries and hashes; current Simple API v1.1 responses can also include file size/upload-time fields when available. Capture the response rather than manually transcribing a hash.

10. Consume from a clean environment

Create a new venv after publication. Reuse only the isolated HOME/netrc and pip route, not the producer environment or its package installation state.

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

python -m pip install --no-deps --no-cache-dir learner-ch09-checkpoint==1.0.0   -vv 2>&1 | tee "$LAB/evidence/fresh-install.txt"

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

Review fresh-install.txt: it should identify only the Nexus internal group as the package index. There should be no direct pypi.org index URL.

11. Retrieve the wheel and prove byte identity independently

python -m pip download --no-deps --only-binary=:all: --no-cache-dir   learner-ch09-checkpoint==1.0.0   -d "$LAB/retrieved"   2>&1 | tee "$LAB/evidence/retrieval.txt"

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

Compare the retrieved wheel hash against the producer hash and Simple API hash. All three should agree. This is the artifact identity invariant for the checkpoint.

12. Prove the internal namespace cannot fall directly to public PyPI

There are two independent proofs:

  1. The pip config has exactly one index URL: academy-ch09cp-internal/simple, with no extra-index-url and no pypi.org.
  2. The internal group has exactly one member: academy-ch09cp-hosted. The separate tooling proxy is not a member, so a miss cannot be routed there by this endpoint.
python -m pip install --no-cache-dir --no-deps   learner-ch09-checkpoint-does-not-exist==1.0.0   -vv 2>&1 | tee "$LAB/evidence/internal-miss.txt" || true

# Review the output: candidate/index requests must stay on 127.0.0.1
if grep -qi 'pypi.org' "$LAB/evidence/internal-miss.txt"; then
  echo 'FAIL: direct public fallback appeared in pip output' >&2
  exit 1
fi

The expected result is “no matching distribution” or equivalent, not a public lookup. This is a routing proof, not a security scan.

13. Optional restart persistence check

If this checkpoint runs on the disposable Chapter 02 instance, stop/start Nexus cleanly using the supported service/process method. Then repeat the JSON Simple API request and fresh download. The package metadata and bytes should persist because they live under persistent Nexus data/database/blob state, not in the application directory or Python client cache.

14. Required evidence packet

  • Nexus 3.95.2/edition/status plus Java/runtime evidence.
  • Python, pip, build, and Twine versions actually used.
  • Repository topology and deployment policy, including proof that the tooling proxy is not an internal-group member.
  • Redacted privilege matrix for the publisher.
  • Pip route proof showing one internal Nexus index and no direct public fallback.
  • Producer wheel/sdist hashes.
  • Twine upload output with no credential values.
  • Nexus Browse/Search or safe REST evidence for component/assets.
  • HTML and JSON Simple API responses.
  • Fresh-install verbose output.
  • Retrieved wheel SHA-256 and equality comparison.
  • Controlled missing-package failure proving requests remain on loopback Nexus.
  • Cleanup checklist.

15. Cleanup and rollback

Delete only the checkpoint resources. First remove the internal group, then the public tooling proxy and hosted repository, through Nexus UI/API. Remove the disposable publisher user/role. Do not manually delete the default blob store or database rows—the default blob store may contain unrelated lab content from earlier chapters. If storage reclamation is desired, use the current supported cleanup/compaction semantics introduced later in the course.

deactivate 2>/dev/null || true
rm -f "$LAB/home/.netrc" "$LAB/home/pip.conf"
unset HOME PIP_CONFIG_FILE PIP_CACHE_DIR NX_USER

printf 'If evidence has been copied to a safe location, remove local lab directory: %s
' "$LAB"

16. Final verification checklist

  • The package was built once for version 1.0.0 and uploaded only to hosted.
  • Hosted deployment policy was Disable redeploy.
  • The internal group contained only the hosted repository; the public tooling proxy remained separate.
  • After tooling bootstrap, the client had no extra-index-url, no direct PyPI index, and no tooling-proxy URL.
  • Fresh install and fresh download succeeded from the internal group.
  • Producer, Simple API, and retrieved wheel SHA-256 values matched.
  • The synthetic missing package failed without a direct public request.
  • No credential was printed into evidence or passed as a command-line password argument.
  • No Nexus blob/database internals were edited or deleted.

Knowledge check

Why does the checkpoint keep the tooling proxy outside the internal group?

What three independent places should agree on the wheel identity?

If the missing-package test contacts pypi.org, what failed?

Why should the default blob store not be deleted during cleanup?

What does the restart test prove?

What does Chapter 09 add to a production artifact-repository operating model?

17. Production operating model and Chapter 10 bridge

You can now operate Python package flow as a governed index protocol rather than as “pip points at a URL.” A production design can control where candidates are discovered, keep public egress behind Nexus, separate publication from consumption, prevent accidental release mutation, verify exact distribution bytes, and recognize when source builds introduce executable supply-chain steps.

Chapter 10 moves to NuGet and .NET package feeds. The same hosted/proxy/group and evidence disciplines remain, but package IDs/versions, V2/V3 feed behavior, API keys, symbols, and .NET client configuration introduce a different protocol model.

Next chapter

NuGet feeds, API keys, and symbols

Carry forward immutable package identity, controlled client endpoints, least-privilege publication, and cache diagnostics into the .NET package ecosystem.

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.