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.
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?
Hosted is the authoritative publication target. The Simple API is a consumer index contract, and the group is a read aggregation endpoint in this course.
What changed in Nexus when the wheel and sdist were uploaded?
Repository/component/asset metadata changed in the Nexus database and the exact distribution bytes were added to the hosted repository blob store.
Why use a second venv and cache for consumption?
It prevents the producer environment or local pip cache from masking whether Nexus can actually serve the published package.
What does matching local, Simple API, and retrieved SHA-256 prove?
That the same exact file bytes were built, indexed, and retrieved. It does not prove publisher identity or security.
Why is the Conda fixture valid mandatory learning?
The prompt allows a fixture or supported local client path; the fixture teaches channel/subdir/repodata relationships without imposing a separate Conda installation.
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
- 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.