From 6e63fe3b01fa263ecc81f93ba08b4d9328ad0f5d Mon Sep 17 00:00:00 2001 From: Zachary Spector Date: Sun, 12 Apr 2026 12:23:00 +1200 Subject: [PATCH] Add a minimal JSON template to the PyPI registry --- routers/api/packages/api.go | 1 + routers/api/packages/pypi/pypi.go | 31 +++++++++++++++++++++++++++ templates/api/packages/pypi/json.tmpl | 14 ++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 templates/api/packages/pypi/json.tmpl diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 0b46cc66b0..37891d58b5 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -581,6 +581,7 @@ func CommonRoutes() *web.Route { r.Post("/", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), pypi.UploadPackageFile) r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) r.Get("/simple/{id}", pypi.PackageMetadata) + r.Get("/{id}/json", pypi.JSONPackageMetadata) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/rpm", func() { r.Group("/repository.key", func() { diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go index 360632570e..56bb8905c0 100644 --- a/routers/api/packages/pypi/pypi.go +++ b/routers/api/packages/pypi/pypi.go @@ -75,6 +75,37 @@ func PackageMetadata(ctx *context.Context) { ctx.HTML(http.StatusOK, "api/packages/pypi/simple") } +// JSONPackageMetadata returns the same data as PackageMetadata, but in JSON +func JSONPackageMetadata(ctx *contex.Context) { + packageName := normalizer.Replace(ctx.Params("id")) + + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypePyPI, packageName) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pvs) == 0 { + apiError(ctx, http.StatusNotFound, err) + return + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + // sort package descriptors by version to mimic PyPI format + sort.Slice(pds, func(i, j int) bool { + return strings.Compare(pds[i].Version.Version, pds[j].Version.Version) < 0 + }) + ctx.Data["RegistryURL"] = setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/pypi" + ctx.Data["PackageDescriptor"] = pds[0] + ctx.Data["PackageDescriptors"] = pds + ctx.JSONTemplate(http.StatusOK, "api/packages/pypi/simple") +} + + // DownloadPackageFile serves the content of a package func DownloadPackageFile(ctx *context.Context) { packageName := normalizer.Replace(ctx.Params("id")) diff --git a/templates/api/packages/pypi/json.tmpl b/templates/api/packages/pypi/json.tmpl new file mode 100644 index 0000000000..e5504d7eea --- /dev/null +++ b/templates/api/packages/pypi/json.tmpl @@ -0,0 +1,14 @@ +{ + "info": { + "name": "{{.PackageDescriptor.Package.Name}}", + "urls": [{{range .PackageDescriptors}}{{$p := ..}}{{range .Files}} + { + "digests": { + "sha256": "{{.Blob.HashSHA256}}" + }, + "url": "{{$.RegistryURL}}/files/{{$p.Package.LowerName}}/{{$p.Version.Version}}/{{.File.Name}}"{{if $p.Metadata.RequiresPython}}, + "requires_python": "{{$p.Metadata.RequiresPython}}"{{end}} + } + {{end}}{{end}}] + } +}