Skip to content

PyPI ​

Hosted Python package index: PEP 503 simple index for pip, legacy upload API for twine, and a JSON package endpoint.

Format idpypi
Index URLHOST/repos/REPO/simple/
Upload URLHOST/repos/REPO/legacy/
Repository typeshosted, proxy, group (details)

Client setup ​

sh
pip config set global.index-url HOST/repos/REPO/simple/

For a private repository, pip needs credentials for the index URL: put the username and the token (as the password) in the URL userinfo, or use a .netrc entry for the host. The username is ignored by the server.

Upload with twine:

ini
# ~/.pypirc
[distutils]
index-servers = openartifact

[openartifact]
repository = HOST/repos/REPO/legacy/
username = USERNAME
password = <TOKEN>
sh
twine upload --repository openartifact dist/*

The upload URL ends in /legacy/.

Endpoints ​

Relative to HOST/repos/REPO:

RequestBehaviour
GET /simple/HTML index of all project names
GET /simple/<package>/HTML file list; links are /repos/REPO/files/<file>#sha256=<digest>, with data-requires-python when the upload set it
GET /files/<filename>download a distribution
POST /legacy/upload (multipart), 200 on success
DELETE /files/<filename>delete one distribution, 200
GET /json/<package>info (name, version, summary, author, author_email, license, taken from the first stored file) and releases (per file: filename, url, digests)

Package names are normalized (lowercase, each _ and . becomes -) on upload, so My_Package and my-package are one project.

Upload ​

twine sends a multipart form to POST /legacy/:

  • content is the file (wheel or sdist); a missing part is 400 missing file. The file is stored as sent, with its MD5 and SHA-256 computed on the server.
  • Metadata fields read: name, version, filetype, pyversion, requires_python, summary, author, author_email, license, keywords, classifiers (newline-separated).
  • name and version must be present, and the file name must belong to them (<dist>-<version>.tar.gz, or <dist>-<version>-<tags>.whl with - in the version written _); otherwise 400. Names are normalized per PEP 503 (runs of -_. become one -).
  • A file name that already exists in the repository is 409 file already exists: distributions are immutable, bump the version instead. This also covers a file name another name/version pair already holds (foo-1-2.tar.gz is both foo 1-2 and foo-1 2); a download of an already ambiguous file name is 409.
  • The upload body is capped at 512 MiB of file plus 1 MiB of form fields (413 above it). The file is read fully into memory to compute its SHA-256; the repository's storage quota is enforced (507 over quota).
  • Downloads of quarantined files are refused.

Proxy and group ​

  • Proxy: pip install --index-url HOST/repos/PROXY/simple/ <package> fetches the simple pages from upstream_url (default https://pypi.org/simple), rewrites every file link to HOST/repos/PROXY/files/<project>/<filename> keeping the hash, requires-python, metadata and yanked data, and caches pages (for cache_ttl_seconds, served stale when the upstream is down) and distribution files (immutable, verified against the index sha256). HTML and PEP 691 JSON are both served, chosen by Accept. Uploads are 405.
  • Group: an ordered list of hosted and proxy pypi members. simple/ and simple/<project>/ are merged across members (union by filename, the first member wins a duplicate) and linked to the group; files are served from the first member that has them. Use --index-url HOST/repos/GROUP/simple/.

Details, including failure behavior and credentials: hosted, proxy, group.

Signing ​

PyPI repositories have no repository signature; clients verify the sha256 fragment on each file link. GPG signing covers only Debian and RPM (signing).

Free software under the AGPL-3.0-or-later.