Working with PyPI¶
Proxy Repository¶
curl -X POST https://save.example.com/api/v1/repos \
-H "Content-Type: application/json" \
-u "<username>:<password>" \
-d '{
"project": "backend",
"name": "pypi-proxy",
"format": "pypi",
"repository_type": "proxy",
"remote_url": "https://pypi.org/",
"cache_ttl": 3600
}'
Hosted Repository¶
curl -X POST https://save.example.com/api/v1/repos \
-H "Content-Type: application/json" \
-u "<username>:<password>" \
-d '{
"project": "backend",
"name": "pypi-hosted",
"format": "pypi",
"repository_type": "hosted"
}'
After creation, the repository is available at https://save.example.com/pypi/<project>/<repository>/. The simple index is available under /simple/; package upload (twine-compatible POST) uses the repository root.
Client Configuration¶
pip¶
# One-time installation through --index-url
pip install --index-url https://save.example.com/pypi/<project>/pypi-proxy/simple/ <package>
# Persistent configuration through pip.conf
cat > ~/.config/pip/pip.conf << EOF
[global]
index-url = https://USER:PASS@save.example.com/pypi/<project>/pypi-proxy/simple/
trusted-host = save.example.com
EOF
pip.conf location
- Linux/macOS:
~/.config/pip/pip.confor~/.pip/pip.conf - Windows:
%APPDATA%\pip\pip.ini - In virtualenv:
$VIRTUAL_ENV/pip.conf
Robot accounts in CI
For CI/CD, use a robot account: username = sa$<robot-name>, password = <api-key>. The pip.conf structure remains the same; only username changes. For details, see Authentication.
poetry¶
In pyproject.toml:
[[tool.poetry.source]]
name = "codescoring"
url = "https://save.example.com/pypi/<project>/pypi-proxy/simple/"
priority = "primary"
Credentials are passed through poetry config:
pipenv¶
In Pipfile:
[[source]]
url = "https://USER:PASS@save.example.com/pypi/<project>/pypi-proxy/simple/"
verify_ssl = true
name = "codescoring"
uv¶
Or in pyproject.toml:
[[tool.uv.index]]
name = "codescoring"
url = "https://save.example.com/pypi/<project>/pypi-proxy/simple/"
default = true
Publishing (twine, hosted)¶
Packages are uploaded to a hosted repository with standard twine upload. The upload URL is the repository root without /simple/.
# ~/.pypirc
[distutils]
index-servers = codescoring
[codescoring]
repository = https://save.example.com/pypi/<project>/pypi-hosted/
username = <username>
password = <password>
Repository URL Migration¶
Use case: migrating a PyPI repository from Nexus / Artifactory to CodeScoring.Save.
| Source | URL in pip.conf before migration | URL in pip.conf after migration |
|---|---|---|
| Nexus | https://nexus.host.ru/repository/pypi-proxy/simple |
https://save.example.com/pypi/<project>/pypi-proxy/simple/ |
| Artifactory | https://jfrog.host.ru/artifactory/api/pypi/pypi-remote/simple |
https://save.example.com/pypi/<project>/pypi-proxy/simple/ |
| Official repository | https://pypi.org/simple |
https://save.example.com/pypi/<project>/pypi-proxy/simple/ |
Authentication parameters (username / password) and the pip.conf format remain unchanged.
Troubleshooting¶
Checking the Simple Index¶
curl -u "<username>:<password>" \
https://save.example.com/pypi/<project>/pypi-proxy/simple/<package>/
The response is an HTML page with links to package files. If the page is empty or returns 404, the package is not cached and upstream did not return it.
Service Status¶
Repository Audit¶
curl -u "<username>:<password>" \
"https://save.example.com/api/v1/admin/audit?resource_type=repository&q=pypi-proxy&limit=50"