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 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:
-
The pip config has exactly one index URL:
academy-ch09cp-internal/simple, with noextra-index-urland nopypi.org. -
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?
It lets the lab bootstrap build tools through Nexus while making internal package discovery provably hosted-only; no public candidate can enter through the internal group.
What three independent places should agree on the wheel identity?
The producer-side SHA-256, the hash advertised by the Simple API, and the SHA-256 of the freshly retrieved wheel.
If the missing-package test contacts pypi.org, what failed?
The client routing contract failed: a direct public index or fallback is still configured. Fix the client/Nexus topology before calling the namespace controlled.
Why should the default blob store not be deleted during cleanup?
It may contain other repositories and lab content. Delete only the disposable repositories through supported Nexus operations.
What does the restart test prove?
That repository metadata and package bytes persist in Nexus persistent data/database/blob state rather than depending on transient process or client cache state.
What does Chapter 09 add to a production artifact-repository operating model?
A Python-specific index model with controlled candidate discovery, immutable publication, wheel/sdist execution awareness, isolated credentials/client cache, hash evidence, and explicit PyPI-versus-Conda protocol boundaries.
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.
Official references and version notes
- Nexus Repository Download and 3.95.0–3.95.2 release notes — pinned self-hosted baseline and current PyPI fixes.
- Sonatype: PyPI Repositories, Create a PyPI Repository, and Configure PyPI with Nexus.
- Sonatype: PyPI CLI Usage — pip, uv, Poetry, and Twine client workflows.
- Sonatype: Conda Repositories, Create a Conda Repository, and Configure Conda with Nexus.
-
pip install documentation
— index selection, cache behavior, and the dependency-confusion
warning for
--extra-index-url. - Python Packaging User Guide: Simple Repository API.
- Python Packaging Flow and Packaging Python Projects.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.