API Reference

Cornucopia REST API endpoints

Cornucopia API Documentation

Base URL

All endpoints are relative to the Cornucopia base URL (e.g., http://localhost:8080).

Endpoints

GET /health

Health check for load balancers and monitoring.

Response:

{"status": "ok"}

Status Codes:

  • 200: Server is healthy

Package Indexes (PEP 503 & PEP 691)

GET /simple/

List all known packages in the registry.

Query Parameters: None

Content Negotiation:

  • Accept: text/html → PEP 503 HTML format (default)
  • Accept: application/vnd.pypi.simple.v1+json → PEP 691 JSON format

HTML Response (PEP 503):

<!DOCTYPE html>
<html>
  <head><title>Simple Index</title></head>
  <body>
    <a href="/simple/requests/">requests</a>
    <a href="/simple/numpy/">numpy</a>
  </body>
</html>

JSON Response (PEP 691):

{
  "meta": {"api-version": "1.0"},
  "projects": ["requests", "numpy", "django"]
}

Status Codes:

  • 200: Success
  • 500: Internal error

GET /simple/{name}/

List all versions and files for a specific package.

Path Parameters:

  • name (required): Package name (normalized or non-normalized, case-insensitive)

Content Negotiation: Same as /simple/

Redirection: If name is not normalized (e.g., Django), responds with 301 redirect to normalized form (django). This is required by PEP 503.

HTML Response (PEP 503):

<!DOCTYPE html>
<html>
  <head><title>Links for requests</title></head>
  <body>
    <h1>Links for requests</h1>
    <a href="/packages/requests-2.31.0-py3-none-any.whl#sha256=abcdef1234567890"
       data-requires-python=">=3.7">requests-2.31.0-py3-none-any.whl</a>
    <a href="/packages/requests-2.31.0.tar.gz#sha256=fedcba0987654321"
       data-requires-python=">=3.7">requests-2.31.0.tar.gz</a>
  </body>
</html>

Important: The #sha256= fragment is mandatory per PEP 503. pip uses it for integrity verification.

JSON Response (PEP 691):

{
  "meta": {"api-version": "1.0"},
  "name": "requests",
  "files": [
    {
      "filename": "requests-2.31.0-py3-none-any.whl",
      "url": "/packages/requests-2.31.0-py3-none-any.whl",
      "hashes": {
        "sha256": "abcdef1234567890",
        "md5": "1234567890abcdef"
      },
      "requires-python": ">=3.7",
      "yanked": false
    },
    {
      "filename": "requests-2.31.0.tar.gz",
      "url": "/packages/requests-2.31.0.tar.gz",
      "hashes": {
        "sha256": "fedcba0987654321",
        "md5": "abcdef1234567890"
      },
      "requires-python": ">=3.7",
      "yanked": false
    }
  ]
}

Status Codes:

  • 200: Package found
  • 301: Non-normalized name (redirect to normalized)
  • 404: Package not found (on first request, may fetch from upstream)
  • 500: Internal error

Package Download

GET /packages/{filename}

Download a package file.

Path Parameters:

  • filename (required): Full filename (e.g., requests-2.31.0-py3-none-any.whl)

Behavior:

  1. Check local cache
  2. If not cached, fetch from upstream PyPI
  3. Validate checksum
  4. Return file

Response Headers:

  • Content-Type: Appropriate MIME type (application/zip for wheels, application/gzip for sdists, etc.)
  • Content-Length: File size
  • Content-Disposition: attachment; filename="{filename}"
  • Cache-Control: public, immutable, max-age=31536000 (files are immutable)

Status Codes:

  • 200: File served successfully
  • 404: File or parent package not found
  • 500: Download or checksum validation failed

Example:

curl -O http://localhost:8080/packages/requests-2.31.0.tar.gz

Package Upload (twine)

POST /legacy/

Upload a package file (twine-compatible endpoint).

Authentication: HTTP Basic Auth

  • Username: __token__
  • Password: API token (configured server-side)

Request Format: multipart/form-data

Form Fields:

  • :action (required): Must be file_upload
  • name (required): Package name
  • version (required): Version string
  • sha256_digest (required): SHA256 hash (computed by twine)
  • md5_digest (optional): MD5 hash
  • content (required): File bytes (multipart file field)
  • requires_python (optional): Python version specifier (PEP 440)
  • Other metadata fields (optional): metadata_version, summary, author, etc.

Response:

File {filename} uploaded successfully

Status Codes:

  • 200: Upload successful
  • 400: Validation error (missing field, malformed)
  • 401: Authentication failed
  • 403: Upload disabled in config
  • 409: File already exists
  • 500: Internal error (checksum mismatch, storage failure)

Example with twine:

twine upload \
  --repository-url http://localhost:8080/legacy/ \
  --username __token__ \
  --password my-secret-token \
  dist/*

Example with curl:

curl -X POST \
  -H "Authorization: Basic $(echo -n '__token__:my-secret-token' | base64)" \
  -F ":action=file_upload" \
  -F "name=mypackage" \
  -F "version=1.0.0" \
  -F "sha256_digest=$(sha256sum dist/mypackage-1.0.0.tar.gz | cut -d' ' -f1)" \
  -F "content=@dist/mypackage-1.0.0.tar.gz" \
  http://localhost:8080/legacy/

Error Responses

HTTP 404

Package or file not found.

{
  "error": "not found"
}

HTTP 500

Internal server error. Check server logs for details.

Internal Server Error

Content Negotiation

Cornucopia supports both PEP 503 (HTML) and PEP 691 (JSON) formats. Choose based on your client:

ClientRecommendedAccept Header
pipEither (auto-detects)text/html, application/vnd.pypi.simple.v1+json, */*;q=0.01
poetryJSONapplication/vnd.pypi.simple.v1+json
pdmEitherEither
Manual browserHTMLtext/html

PEP 503 Compliance

Cornucopia implements PEP 503 (Simple Repository API) and PEP 691 (JSON-based Simple API):

  • ✅ Normalized package names (lowercase, dashes)
  • ✅ 301 redirects for non-normalized names
  • ✅ Mandatory #sha256= fragments in HTML
  • ✅ PEP 440 version specifiers in data-requires-python
  • ✅ Both HTML and JSON formats

Caching Behavior

Metadata Caching

  • In-memory TTL: Configurable (default: 10 minutes)
  • Disk cache: Permanent (until manually deleted)
  • Upstream failure: Falls back to stale metadata if available

File Caching

  • Permanent: Package files are cached forever (PyPI files are immutable)
  • Validation: Checksums validated on first access
  • Deduplication: Concurrent requests for the same file share a single upstream fetch

Rate Limiting

None enforced by Cornucopia. Limits are inherited from upstream PyPI. If you’re rate-limited by PyPI, use --retries with pip and max_retries in Cornucopia config.


Examples

Using pip with Cornucopia

pip install --index-url http://localhost:8080/simple/ requests

Or configure globally in ~/.pip/pip.conf:

[global]
index-url = http://localhost:8080/simple/

Using poetry

poetry source add cornucopia http://localhost:8080/simple/
poetry add requests -S cornucopia

Using pdm

pdm config pypi.url http://localhost:8080/simple/
pdm add requests

Uploading internal packages

twine upload --repository-url http://localhost:8080/legacy/ dist/*

Monitoring

Health Check

curl http://localhost:8080/health
# {"status":"ok"}

Checking Cache

Look in the configured cache directory:

ls ./cache/packages/          # Downloaded files
ls ./cache/meta/              # Package metadata (JSON)

Request Logging

Check stdout/stderr for structured logs (format: METHOD PATH STATUS DURATION):

GET /simple/requests/ 200 123.456ms
GET /packages/requests-2.31.0.tar.gz 200 5.123s