Skip to content

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.conf or ~/.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:

poetry config http-basic.codescoring <username> <password>

pipenv

In Pipfile:

[[source]]
url = "https://USER:PASS@save.example.com/pypi/<project>/pypi-proxy/simple/"
verify_ssl = true
name = "codescoring"

uv

uv pip install --index-url https://save.example.com/pypi/<project>/pypi-proxy/simple/ <package>

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>
python -m build
twine upload -r codescoring dist/*

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

curl https://save.example.com/health

Repository Audit

curl -u "<username>:<password>" \
  "https://save.example.com/api/v1/admin/audit?resource_type=repository&q=pypi-proxy&limit=50"
Страница была полезна?