Move your packages off devpi, Artifactory, or Nexus¶
Pull your private packages out of an old index and into pypiron in one command. They land as your own packages — private, served from your index, never fetched from PyPI.
pypiron sync \
--from https://devpi.corp/team/dev/+simple \
--source-user "$SRC_USER" --source-pass "$SRC_PASS" \
--as-private \
--to http://localhost:8080 \
--admin-user admin --admin-pass "$PYPIRON_ADMIN_PASS" \
--include-package internal-app \
--include-package internal-lib
Each named package is downloaded from the old index (authenticated) and re-uploaded to pypiron as private. Install it like anything else:
What migrating does — and doesn't¶
- Packages land private: your own packages, served from your index. Not a mirror of a public project — pypiron never falls through to PyPI for them.
- Timestamps and yank state are not preserved. Migrated files carry the migration date. (Mirroring public PyPI keeps real upload times; a private migration doesn't.)
- Don't set
--private-prefixduring a migration. It reserves a namespace for private names, and a package whose name falls outside it is refused. Migrate first, add the prefix after.
Re-running is safe: files already migrated are skipped, so a second pass only carries what's new.
devpi¶
devpi serves each index's package list at <base>/<user>/<index>/+simple. Point
--from at that:
pypiron sync \
--from https://devpi.example.com/acme/prod/+simple \
--source-user acme --source-pass "$DEVPI_PASS" \
--as-private \
--to http://localhost:8080 \
--admin-user admin --admin-pass "$PYPIRON_ADMIN_PASS" \
--include-package acme-billing --include-package acme-auth
Artifactory and Nexus¶
Same command, different source URL — point --from at the repository's simple
endpoint:
- Artifactory:
https://<host>/artifactory/api/pypi/<repo>/simple - Nexus:
https://<host>/repository/<repo>/simple
pypiron sync \
--from https://nexus.example.com/repository/pypi-internal/simple \
--source-user "$SRC_USER" --source-pass "$SRC_PASS" \
--as-private \
--to http://localhost:8080 \
--admin-user admin --admin-pass "$PYPIRON_ADMIN_PASS" \
--include-package internal-app
One prerequisite: the source must serve the JSON simple API (PEP 691). pypiron reads the modern JSON index; it does not scrape the older HTML index.
- Artifactory serves HTML by default. Turn on JSON per repository: Administration > Artifactory Settings > Packages Settings > PyPI > Enable simple json format. (If JSON is off, Artifactory falls back to HTML.)
- Nexus serves the JSON simple API from 3.93 onward (3.94 adds file sizes and upload times). Older Nexus is HTML-only — upgrade before migrating.
If the source is still HTML-only, the migration stops with a clear message instead of a cryptic parse error:
source returned an HTML page (Content-Type: text/html), not the PEP 691 JSON
pypiron migration requires — point --from at a JSON-capable endpoint, or check
credentials if this is a login page.
The same message appears if a wrong credential lands you on a login page — check
--source-user/--source-pass before assuming the endpoint is wrong.
devpi is tested end-to-end. The Artifactory and Nexus paths above are their standard simple-API endpoints; the JSON-indexing prerequisite is the one thing to confirm first.
Migrating everything¶
sync migrates the packages you name — it won't enumerate the whole source for
you. Most teams already know their list. If you don't, read it from the source
index once:
curl -s -u "$SRC_USER:$SRC_PASS" \
-H 'Accept: application/vnd.pypi.simple.v1+json' \
https://devpi.example.com/acme/prod/+simple/ \
| python3 -c 'import sys, json; print("\n".join(p["name"] for p in json.load(sys.stdin)["projects"]))' \
> packages.txt
pypiron sync --from https://devpi.example.com/acme/prod/+simple \
--source-user "$SRC_USER" --source-pass "$SRC_PASS" --as-private \
--to http://localhost:8080 --admin-user admin --admin-pass "$PYPIRON_ADMIN_PASS" \
--include-packages-from packages.txt
Credentials¶
Keep passwords out of the command line — use the environment:
PYPIRON_SYNC_SOURCE_USER / PYPIRON_SYNC_SOURCE_PASS for the source,
PYPIRON_SYNC_ADMIN_PASS for pypiron. Source credentials go to the source host
only; they are never forwarded to a redirect somewhere else.
Full flag list: Configuration → Sync.