Documentation

Publish packages

velodex accepts the legacy upload API, the wire protocol both twine and uv publish speak. Uploads need a hosted index with an upload_token; the default topology's hosted index has none, so uploads are off until you set one:

[[index]]
name = "pypi"
cached = "https://pypi.org/simple/"

[[index]]
name = "hosted"
upload_token = "<secret>"

[[index]]
name = "root/pypi"
layers = ["hosted", "pypi"]
upload = "hosted"

Then publish to the virtual index's route. velodex accepts any username; the token is the password, matching the pypi.org __token__ convention:

twine upload --repository-url http://127.0.0.1:4433/root/pypi/ -u __token__ -p <secret> dist/*
# or
uv publish --publish-url http://127.0.0.1:4433/root/pypi/ -u __token__ -p <secret> dist/*

velodex accepts wheels and modern .tar.gz source distributions. It rejects .egg, .zip, and ambiguous legacy archives on upload; those files can still be mirrored if an upstream index lists them. During upload, velodex checks the declared sha256 and blake2b-256 digests while streaming the artifact into a staged blob. A lone md5 digest is rejected.

Before publishing the staged blob, velodex validates the project name, PEP 440 version, safe filename shape, filetype, archive readability, and metadata identity. Wheel uploads must contain one normalized {name}-{version}.dist-info/ directory with METADATA, WHEEL, and RECORD. The WHEEL tags and optional build field must match the filename, and RECORD must cover each archive file except RECORD and deprecated RECORD signatures with sha256-or-better hashes. When RECORD includes a size, the size must match the archive member.

Modern sdists must be .tar.gz files whose filename follows PEP 625. The archive must contain one top-level {name}-{version}/ directory with pyproject.toml and PKG-INFO. velodex rejects tar entries with absolute paths, traversal, unsafe links, special files, or device entries. For Metadata 2.4 and newer, every License-File header must name a file inside the sdist.

The filename, form fields, and METADATA or PKG-INFO Name and Version must agree. Metadata-Version, Requires-Python, license fields, extras, and project URLs are compared when the upload form supplies them and the metadata model can represent them. Requires-Python, when present in the form or metadata, must parse as Python version specifiers.

Accepted files are stored content-addressed and served from /root/pypi/simple/<project>/ alongside the cached index's packages. Your file shadows an upstream file of the same name. For wheels, velodex extracts METADATA; for sdists, it extracts the verified PKG-INFO. Both are served as PEP 658/714 .metadata siblings, so resolvers get the fast path for your uploads and the web UI can show the full package page.

In .pypirc

The .pypirc file holds the repository and credentials:

[distutils]
index-servers = velodex

[velodex]
repository = http://127.0.0.1:4433/root/pypi/
username = __token__
password = <secret>

twine upload -r velodex dist/* then works without flags.

GET /root/pypi/+api returns the same .pypirc shape when the request reaches Velodex with the public Host header. The discovery document keeps the password as <upload-token>; replace it with the hosted index token before publishing. For offline setup, print the same snippet from the config file:

velodex config-snippet --base-url http://127.0.0.1:4433 --index root/pypi .pypirc

Upload failures

Validation failures return 400 with the field or archive check that failed. Common causes:

  • The filename is not a wheel or .tar.gz sdist.
  • The filename's normalized project name or version does not match the form fields.
  • The archive is corrupt, lacks required wheel or sdist files, has unsafe tar entries, or has a bad RECORD.
  • Core metadata names a different project or version.
  • A Metadata 2.4+ sdist lists a License-File that is missing from the archive.
  • A declared sha256 or blake2b-256 digest does not match the received bytes.
  • The same filename was already uploaded with different bytes.
On this page