feat: virtual PyPI repos can merge local + remote members
ci/woodpecker/pr/pre-commit Pipeline was canceled
ci/woodpecker/pr/build Pipeline was canceled
ci/woodpecker/pr/test Pipeline was canceled

The virtual engine now detects local members (repo_type=local) and
generates their package index in-memory instead of trying to fetch
from a non-existent upstream.

- MemberIndex gains RepoType field so mergers use correct URL prefix
  (/api/v1/local/ vs /api/v1/remote/)
- Virtual engine accepts a LocalIndexGenerator interface for producing
  local PyPI indexes
- LocalHandler implements GeneratePyPIPackageHTML for reuse by both
  the direct serving path and the virtual merger
- Includes local PyPI upload support (cherry-picked from benvin/local-pypi)

Tested e2e: local wheel upload + virtual merge + uv pip install from
both direct local and virtual URLs
This commit is contained in:
2026-06-23 22:13:12 +10:00
parent 1b2db07f25
commit b286b19eb4
5 changed files with 81 additions and 35 deletions
+15 -9
View File
@@ -320,10 +320,19 @@ func (h *LocalHandler) ServePyPIPackageIndex(w http.ResponseWriter, r *http.Requ
return
}
body := h.generatePyPIPackageHTML(normalized, files)
var b strings.Builder
b.WriteString("<!DOCTYPE html>\n<html><body>\n")
for _, f := range files {
filename := strings.TrimPrefix(f.FilePath, normalized+"/")
hash := strings.TrimPrefix(f.ContentHash, "sha256:")
fmt.Fprintf(&b, "<a href=\"../../%s/%s#sha256=%s\">%s</a>\n",
normalized, filename, hash, filename)
}
b.WriteString("</body></html>\n")
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write(body)
io.WriteString(w, b.String())
}
func (h *LocalHandler) GeneratePyPIPackageHTML(ctx context.Context, repoName, packageName string) ([]byte, error) {
@@ -333,20 +342,17 @@ func (h *LocalHandler) GeneratePyPIPackageHTML(ctx context.Context, repoName, pa
if err != nil {
return nil, err
}
return h.generatePyPIPackageHTML(normalized, files), nil
}
func (h *LocalHandler) generatePyPIPackageHTML(packageName string, files []database.LocalFile) []byte {
var b strings.Builder
b.WriteString("<!DOCTYPE html>\n<html><body>\n")
for _, f := range files {
filename := strings.TrimPrefix(f.FilePath, packageName+"/")
filename := strings.TrimPrefix(f.FilePath, normalized+"/")
hash := strings.TrimPrefix(f.ContentHash, "sha256:")
fmt.Fprintf(&b, "<a href=\"../../%s/%s#sha256=%s\">%s</a>\n",
packageName, filename, hash, filename)
fmt.Fprintf(&b, "<a href=\"%s/%s#sha256=%s\">%s</a>\n",
normalized, filename, hash, filename)
}
b.WriteString("</body></html>\n")
return []byte(b.String())
return []byte(b.String()), nil
}
func (h *LocalHandler) download(w http.ResponseWriter, r *http.Request) {