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: Success500: 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 found301: 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:
- Check local cache
- If not cached, fetch from upstream PyPI
- Validate checksum
- Return file
Response Headers:
Content-Type: Appropriate MIME type (application/zip for wheels, application/gzip for sdists, etc.)Content-Length: File sizeContent-Disposition:attachment; filename="{filename}"Cache-Control:public, immutable, max-age=31536000(files are immutable)
Status Codes:
200: File served successfully404: File or parent package not found500: 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 befile_uploadname(required): Package nameversion(required): Version stringsha256_digest(required): SHA256 hash (computed by twine)md5_digest(optional): MD5 hashcontent(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 successful400: Validation error (missing field, malformed)401: Authentication failed403: Upload disabled in config409: File already exists500: 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:
| Client | Recommended | Accept Header |
|---|---|---|
| pip | Either (auto-detects) | text/html, application/vnd.pypi.simple.v1+json, */*;q=0.01 |
| poetry | JSON | application/vnd.pypi.simple.v1+json |
| pdm | Either | Either |
| Manual browser | HTML | text/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