PyPI repositories serve Python packages to pip, uv, Poetry, PDM and Pipenv, and take uploads from twine, uv publish, Poetry, Hatch and Flit. Install from the repository's simple/ address and publish to its legacy/ address; sign in with __token__ as the user name and an API token as the password.
pip install --index-url https://__token__:${DEPOTEX_TOKEN}@depotex.example.com/repository/my-space/py/simple/ my-package
twine upload --repository-url https://depotex.example.com/repository/my-space/py/legacy/ -u __token__ -p "$DEPOTEX_TOKEN" dist/*
1. Set up your tools
Each example installs from a repository py in the space my-space and publishes to it; DEPOTEX_TOKEN holds an API token. Keep tokens out of files you commit: each tool below reads them from the environment.
With a PyPI group as the space's default, https://depotex.example.com/repository/my-space/pypi/simple/ is one index for your hosted repositories and PyPI, and https://depotex.example.com/repository/my-space/pypi/legacy/ takes uploads from twine and uv publish, sent to the repository the group's write rules choose by project name.
| Tool | Install from Depotex | Publish to Depotex |
|---|---|---|
| pip | pip.conf (~/.config/pip/pip.conf, or the file in PIP_CONFIG_FILE): [global] index-url = https://__token__:<token>@depotex.example.com/repository/my-space/py/simple/, or the same address in PIP_INDEX_URL | — |
| twine | — | twine upload --repository-url https://depotex.example.com/repository/my-space/py/legacy/ -u __token__ -p $DEPOTEX_TOKEN dist/*, or a [depotex] entry in ~/.pypirc |
| uv | In pyproject.toml: [[tool.uv.index]] with name = "depotex", url = "https://depotex.example.com/repository/my-space/py/simple/" and default = true; credentials in UV_INDEX_DEPOTEX_USERNAME=__token__ and UV_INDEX_DEPOTEX_PASSWORD. uv.lock never stores them. | uv publish --publish-url https://…/legacy/ --username __token__ --password $DEPOTEX_TOKEN |
| Poetry | poetry source add --priority=primary depotex https://…/simple/; credentials in POETRY_HTTP_BASIC_DEPOTEX_USERNAME and POETRY_HTTP_BASIC_DEPOTEX_PASSWORD | POETRY_REPOSITORIES_DEPOTEX_URL=https://…/legacy/, then poetry publish --build -r depotex (same credentials) |
| PDM | [[tool.pdm.source]] with url = "https://${DEPOTEX_USER}:${DEPOTEX_TOKEN}@depotex.example.com/repository/my-space/py/simple/" (PDM fills in the variables) | pdm publish -r https://…/legacy/ -u __token__ -P $DEPOTEX_TOKEN |
Installing from your hosted repository and a pypi.org proxy together can let someone hijack a private name (dependency confusion): pip's --extra-index-url takes the highest version from either index, so a package published on pypi.org under one of your private names can win. Guard against it: with uv, list the hosted repository first (uv takes a package from the first index that has it); with Poetry, make the hosted repository the primary source; with PDM, limit each source with include_packages or exclude_packages; and add a block list rule on the pypi.org proxy for your private names, so they're never fetched from it.
2. Install
These work against hosted and proxy repositories, and are tested with the real tools:
- pip install with pins, ranges, extras (pkg[extra]), environment markers and --pre; source distributions are built by the installer as usual.
- pip download, pip wheel, pip install --require-hashes, pip install --dry-run --report, pip lock, pip index versions, pip list --outdated.
- uv pip install, uv pip compile, uv add, uv lock, uv sync; poetry add and poetry install; pdm add and pdm install.
- pip search works against the repository (pip search --index https://…/repository/my-space/py/pypi term), although pypi.org turned it off.
Every file is listed with its SHA-256, its Python requirement and its core metadata (PEP 658), so installers resolve without downloading every wheel. The JSON API (/pypi/{project}/json) answers tools such as pip-audit.
3. Publish
- Wheels and source distributions, with Publish permission. Depotex checks the file's own metadata against its name and refuses unreadable or unsafe files.
- A file can't be replaced once published: new PyPI repositories publish once, like pypi.org, and publishing a file again answers that it already exists.
- Signatures and PEP 740 attestations sent with an upload are kept.
twine 7 allows --skip-existing only for pypi.org. Use uv publish --check-url <the simple/ address> to skip files the repository already has, or older twine (it skips on the "already exists" answer).
4. Yank a release
A yanked release stays available but installers skip it unless it's pinned exactly, and show the reason. Yank and un-yank from the release's page in the browser (Yank in the quick facts), or with the API, in hosted repositories with Overwrite permission:
curl -X PUT -H "Authorization: Bearer $DEPOTEX_TOKEN" -H "Content-Type: application/json" \
-d '{"yanked": true, "reason": "Broken build"}' \
https://depotex.example.com/api/spaces/my-space/repositories/py/pypi/releases/my-package/1.2.0
5. Versions and policies
- Each release (project and version) is a version: it shows in the Versions tab, and cleanup policies, quotas, quarantine and the block list apply to it.
- Names match however they're written: Foo_Bar, foo.bar and foo-bar are one project, in installs and in quarantine and block-list rules.
- Block-list ranges compare versions as pip does (1.0.post1 after 1.0, 1.1.dev0 before 1.1) and accept Python specifiers such as >=2.0,<2.3.
- Blocked releases are left out of the index; quarantined ones stay listed and their downloads are refused with the reason, unless the repository hides quarantined versions.
6. Proxy repositories
A proxy repository of pypi.org (preset Python Package Index, https://pypi.org/simple) or another index (devpi, Artifactory, Nexus: give its …/simple address) fetches each project's page and files when they're first asked for and keeps them. Every file is checked against the hash the index gives. Files are only fetched from the index's own server, or for pypi.org from files.pythonhosted.org; links elsewhere are refused, and the upstream's credentials are never sent to another server. A proxy can also proxy a PyPI repository of another Depotex (Depotex link).
When the upstream's page doesn't give a file's hash, size or upload date (some indexes' HTML pages don't), Depotex leaves it out instead of guessing; the hash and size appear once the file has been fetched through the proxy. Tools that filter by upload date (uv --exclude-newer) see such files as having no date, not one from 1970.
7. In the artifact browser
Projects show with their releases. The Overview shows the summary, license, required Python version, authors, links, keywords, extras, classifiers, distributions (wheel tags and sdists) and whether the release is yanked; the README tab shows the project description; Dependencies lists Requires-Dist by scope (runtime, or the extra that needs it); PEP 740 attestations show under Security.
8. Limits
- Trusted publishing (publishing from CI with an OIDC identity instead of a token) isn't available yet.
- .tar.xz source distributions aren't accepted.
- A proxy repository's project list holds only projects someone has already fetched through it.
- Environment markers are shown, not evaluated, in the browser's Dependencies tab.