Documentation

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.

How PyPI concepts map to peryx

peryx describes every ecosystem with one neutral vocabulary; for Python it mostly matches the terms you already use, since peryx borrows Python's own words (index, project). The neutral name is what the same idea is called across ecosystems (see the index model and glossary).

Python termperyx conceptWhat it is
index (--index-url)indexthe endpoint a client points at; a cached index proxies one upstream
project / packageprojectone distribution name, like requests
release / versionversionone released version of a project
distribution (wheel/sdist)artifactwhat you install: a .whl or a .tar.gz file
filefileone content-addressed distribution file
publish / uploadupload / publishputting a distribution into a hosted index with twine or uv publish
install / downloaddownloadfetching a distribution through peryx
pull-through mirrorcached (role)a read-through proxy of one upstream index

The role names (cached, hosted, virtual) and shadowing are peryx's own, the same in every ecosystem.

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 peryx 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 publish put 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. peryx serves and understands every current form:

  • PEP 503: the original HTML page of download links. peryx parses it from upstreams that only speak HTML.
  • PEP 691: the modern JSON form of the same data. peryx canonicalizes every upstream to this once, at fetch time, and serves JSON (with HTML on request) downstream.
  • PEP 658/714: a .metadata sibling next to each file, so a resolver reads a few kilobytes of dependency metadata instead of downloading a whole wheel. peryx serves it, and synthesizes it with byte-range reads when an upstream lacks it.
  • Legacy upload API: the POST endpoint twine and uv publish use to publish into a hosted index.

For the full standards map, see standards.

Set me up

Assume peryx 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 peryx http://127.0.0.1:4433/root/pypi/simple/

Publish

Publishing needs a hosted layer with an upload_token. peryx 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 = peryx

[peryx]
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.

In practice

  • Performance

    peryx next to devpi, proxpi, pypiserver, and pypicloud: cold and warm installs, file throughput, a parallel CI fleet, and a request swarm, with the commands behind every number.

  • Tutorials

    Learning-oriented lessons for serving and publishing Python packages through peryx.

  • How-to guides

    Task-oriented recipes for the PyPI ecosystem: publish, cache for CI, compose virtual indexes, yank and delete.

  • Reference

    Information-oriented specifications for the PyPI ecosystem: the Simple-API HTTP endpoints peryx serves.

  • Migration

    Move to peryx from another Python index server: devpi, proxpi, pypiserver, pypicloud, bandersnatch, and more.

On this page