PyPI
PyPI is the Python packaging ecosystem: the format of wheels and sdists, and the HTTP protocol installers use to find
and download them. A wheel (.whl) is a pre-built, ready-to-install package; an sdist (source distribution,
.tar.gz) is the source a wheel is built from. Both are artifacts — the actual files an installer fetches. This is
the one ecosystem velodex ships today.
The roles for PyPI
The three index roles map onto PyPI like this:
- cached — a read-through cache of an upstream Python index such as pypi.org. On a miss velodex fetches the project page or artifact from upstream, stores it, and serves it; later requests come from disk. Point one at pypi.org, a TestPyPI, an Artifactory, or a GitLab registry.
- hosted — a store you publish your own wheels and sdists to over the standard upload API. Nothing upstream; the
files live here because twine or
uv publishput them there. - virtual — an ordered stack of the two, served under one URL, where your hosted uploads shadow same-named upstream
files. This is what clients point at: one
index-url, private packages winning over public ones, no--extra-index-url.
The wire protocol
Python installers speak the Simple API: an index exposes a page per project listing that project's files, and the installer downloads what it resolves. velodex serves and understands every current form:
- PEP 503 — the original HTML page of download links. velodex parses it from upstreams that only speak HTML.
- PEP 691 — the modern JSON form of the same data. velodex canonicalizes every upstream to this once, at fetch time, and serves JSON (with HTML on request) downstream.
- PEP 658/714 — a
.metadatasibling next to each file, so a resolver reads a few kilobytes of dependency metadata instead of downloading a whole wheel. velodex serves it, and synthesizes it with byte-range reads when an upstream lacks it. - Legacy upload API — the POST endpoint twine and
uv publishuse to publish into a hosted index.
For the full standards map, see standards.
Set me up
Assume velodex is running at http://127.0.0.1:4433 with the default virtual route root/pypi. Installers read from
.../simple/; publishers post to the route root.
Install
# one-off
pip install --index-url http://127.0.0.1:4433/root/pypi/simple/ requests
# persistent: environment
export PIP_INDEX_URL=http://127.0.0.1:4433/root/pypi/simple/
# persistent: pip.conf (~/.config/pip/pip.conf or venv pip.conf)
# [global]
# index-url = http://127.0.0.1:4433/root/pypi/simple/
# one-off
uv pip install --index-url http://127.0.0.1:4433/root/pypi/simple/ requests
# persistent: environment
export UV_INDEX_URL=http://127.0.0.1:4433/root/pypi/simple/
poetry source add --priority=primary velodex http://127.0.0.1:4433/root/pypi/simple/
Publish
Publishing needs a hosted layer with an upload_token. velodex accepts any username; the token
is the password, matching pypi.org's __token__ convention.
twine upload --repository-url http://127.0.0.1:4433/root/pypi/ -u __token__ -p <token> dist/*
uv publish --publish-url http://127.0.0.1:4433/root/pypi/ -u __token__ -p <token> dist/*
# ~/.pypirc
[distutils]
index-servers = velodex
[velodex]
repository = http://127.0.0.1:4433/root/pypi/
username = __token__
password = <token>
GET /root/pypi/+api returns a ready-made .pypirc snippet for any configured route.
Other ecosystems
OCI (container images) and npm (JavaScript) are planned: the same cached/hosted/virtual roles apply, driven by each format's own protocol. The capability matrix tracks where each stands.
In practice
- Front an index that is not pypi.org: front another index
- Add credentials for a private upstream: proxy a private upstream
- Publish your own packages: publish