PyPI
Hosted Python package index: PEP 503 simple index for pip, legacy upload API for twine, and a JSON package endpoint.
| Format id | pypi |
| Index URL | HOST/repos/REPO/simple/ |
| Upload URL | HOST/repos/REPO/legacy/ |
| Repository types | hosted, proxy, group (details) |
Client setup
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:
# ~/.pypirc
[distutils]
index-servers = openartifact
[openartifact]
repository = HOST/repos/REPO/legacy/
username = USERNAME
password = <TOKEN>twine upload --repository openartifact dist/*The upload URL ends in /legacy/.
Endpoints
Relative to HOST/repos/REPO:
| Request | Behaviour |
|---|---|
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/:
contentis the file (wheel or sdist); a missing part is400 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). nameandversionmust be present, and the file name must belong to them (<dist>-<version>.tar.gz, or<dist>-<version>-<tags>.whlwith-in the version written_); otherwise400. 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.gzis bothfoo1-2andfoo-12); a download of an already ambiguous file name is409. - The upload body is capped at 512 MiB of file plus 1 MiB of form fields (
413above it). The file is read fully into memory to compute its SHA-256; the repository's storage quota is enforced (507over quota). - Downloads of quarantined files are refused.
Proxy and group
- Proxy:
pip install --index-url HOST/repos/PROXY/simple/ <package>fetches the simple pages fromupstream_url(defaulthttps://pypi.org/simple), rewrites every file link toHOST/repos/PROXY/files/<project>/<filename>keeping the hash,requires-python, metadata and yanked data, and caches pages (forcache_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 byAccept. Uploads are405. - Group: an ordered list of hosted and proxy pypi members.
simple/andsimple/<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).