From 3cb2cacfa2dd75e18a87d912cbee6e6f89b242ed Mon Sep 17 00:00:00 2001 From: Jitka Obselkova Date: Mon, 8 Sep 2025 23:51:45 +0200 Subject: [PATCH 1/2] Add JSON-based Simple API --- ...packagecontent_sha256_metadata_and_more.py | 30 ++++++ pulp_python/app/models.py | 3 + pulp_python/app/pypi/views.py | 88 +++++++++++++++-- pulp_python/app/tasks/publish.py | 1 + pulp_python/app/utils.py | 63 +++++++++++- .../tests/functional/api/test_full_mirror.py | 4 +- .../api/test_pypi_simple_json_api.py | 98 +++++++++++++++++++ 7 files changed, 277 insertions(+), 10 deletions(-) create mode 100644 pulp_python/app/migrations/0016_pythonpackagecontent_sha256_metadata_and_more.py create mode 100644 pulp_python/tests/functional/api/test_pypi_simple_json_api.py diff --git a/pulp_python/app/migrations/0016_pythonpackagecontent_sha256_metadata_and_more.py b/pulp_python/app/migrations/0016_pythonpackagecontent_sha256_metadata_and_more.py new file mode 100644 index 00000000..119d5fd4 --- /dev/null +++ b/pulp_python/app/migrations/0016_pythonpackagecontent_sha256_metadata_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.24 on 2025-09-19 11:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("python", "0015_alter_pythonpackagecontent_options"), + ] + + operations = [ + migrations.AddField( + model_name="pythonpackagecontent", + name="sha256_metadata", + field=models.CharField(default="", max_length=64), + preserve_default=False, + ), + migrations.AddField( + model_name="pythonpackagecontent", + name="yanked", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="pythonpackagecontent", + name="yanked_reason", + field=models.TextField(default=""), + preserve_default=False, + ), + ] diff --git a/pulp_python/app/models.py b/pulp_python/app/models.py index 3bd9d605..eab7012d 100644 --- a/pulp_python/app/models.py +++ b/pulp_python/app/models.py @@ -192,6 +192,9 @@ class PythonPackageContent(Content): packagetype = models.TextField(choices=PACKAGE_TYPES) python_version = models.TextField() sha256 = models.CharField(db_index=True, max_length=64) + sha256_metadata = models.CharField(max_length=64) + yanked = models.BooleanField(default=False) + yanked_reason = models.TextField() # From pulpcore PROTECTED_FROM_RECLAIM = False diff --git a/pulp_python/app/pypi/views.py b/pulp_python/app/pypi/views.py index bd8bc2af..b1301139 100644 --- a/pulp_python/app/pypi/views.py +++ b/pulp_python/app/pypi/views.py @@ -3,7 +3,9 @@ from aiohttp.client_exceptions import ClientError from rest_framework.viewsets import ViewSet +from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer, TemplateHTMLRenderer from rest_framework.response import Response +from rest_framework.exceptions import NotAcceptable from django.core.exceptions import ObjectDoesNotExist from django.shortcuts import redirect from datetime import datetime, timezone, timedelta @@ -43,7 +45,9 @@ ) from pulp_python.app.utils import ( write_simple_index, + write_simple_index_json, write_simple_detail, + write_simple_detail_json, python_content_to_json, PYPI_LAST_SERIAL, PYPI_SERIAL_CONSTANT, @@ -57,6 +61,17 @@ ORIGIN_HOST = settings.CONTENT_ORIGIN if settings.CONTENT_ORIGIN else settings.PYPI_API_HOSTNAME BASE_CONTENT_URL = urljoin(ORIGIN_HOST, settings.CONTENT_PATH_PREFIX) +PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html" +PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json" + + +class PyPISimpleHTMLRenderer(TemplateHTMLRenderer): + media_type = PYPI_SIMPLE_V1_HTML + + +class PyPISimpleJSONRenderer(JSONRenderer): + media_type = PYPI_SIMPLE_V1_JSON + class PyPIMixin: """Mixin to get index specific info.""" @@ -235,6 +250,25 @@ class SimpleView(PackageUploadMixin, ViewSet): ], } + def perform_content_negotiation(self, request, force=False): + """ + Uses standard content negotiation, defaulting to HTML if no acceptable renderer is found. + """ + try: + return super().perform_content_negotiation(request, force) + except NotAcceptable: + return TemplateHTMLRenderer(), TemplateHTMLRenderer.media_type # text/html + + def get_renderers(self): + """ + Uses custom renderers for PyPI Simple API endpoints, defaulting to standard ones. + """ + if self.action in ["list", "retrieve"]: + # Ordered by priority if multiple content types are present + return [TemplateHTMLRenderer(), PyPISimpleHTMLRenderer(), PyPISimpleJSONRenderer()] + else: + return [JSONRenderer(), BrowsableAPIRenderer()] + @extend_schema(summary="Get index simple page") def list(self, request, path): """Gets the simple api html page for the index.""" @@ -242,7 +276,16 @@ def list(self, request, path): if self.should_redirect(repo_version=repo_version): return redirect(urljoin(self.base_content_url, f"{path}/simple/")) names = content.order_by("name").values_list("name", flat=True).distinct().iterator() - return StreamingHttpResponse(write_simple_index(names, streamed=True)) + media_type = request.accepted_renderer.media_type + + if media_type == PYPI_SIMPLE_V1_JSON: + index_data = write_simple_index_json(names) + headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)} + return Response(index_data, headers=headers) + else: + index_data = write_simple_index(names, streamed=True) + kwargs = {"content_type": media_type} + return StreamingHttpResponse(index_data, **kwargs) def pull_through_package_simple(self, package, path, remote): """Gets the package's simple page from remote.""" @@ -252,7 +295,12 @@ def parse_package(release_package): stripped_url = urlunsplit(chain(parsed[:3], ("", ""))) redirect_path = f"{path}/{release_package.filename}?redirect={stripped_url}" d_url = urljoin(self.base_content_url, redirect_path) - return release_package.filename, d_url, release_package.digests.get("sha256", "") + return { + "filename": release_package.filename, + "url": d_url, + "sha256": release_package.digests.get("sha256", ""), + # todo: more fields? + } rfilter = get_remote_package_filter(remote) if not rfilter.filter_project(package): @@ -269,7 +317,7 @@ def parse_package(release_package): except TimeoutException: return HttpResponse(f"{remote.url} timed out while fetching {package}.", status=504) - if d.headers["content-type"] == "application/vnd.pypi.simple.v1+json": + if d.headers["content-type"] == PYPI_SIMPLE_V1_JSON: page = ProjectPage.from_json_data(json.load(open(d.path, "rb")), base_url=url) else: page = ProjectPage.from_html(package, open(d.path, "rb").read(), base_url=url) @@ -290,7 +338,15 @@ def retrieve(self, request, path, package): return redirect(urljoin(self.base_content_url, f"{path}/simple/{normalized}/")) packages = ( content.filter(name__normalize=normalized) - .values_list("filename", "sha256", "name") + .values_list( + "filename", + "sha256", + "name", + "sha256_metadata", + "requires_python", + "yanked", + "yanked_reason", + ) .iterator() ) try: @@ -300,8 +356,28 @@ def retrieve(self, request, path, package): else: packages = chain([present], packages) name = present[2] - releases = ((f, urljoin(self.base_content_url, f"{path}/{f}"), d) for f, d, _ in packages) - return StreamingHttpResponse(write_simple_detail(name, releases, streamed=True)) + releases = ( + { + "filename": f, + "url": urljoin(self.base_content_url, f"{path}/{f}"), + "sha256": s, + "sha256_metadata": sm, + "requires_python": rp, + "yanked": y, + "yanked_reason": yr, + } + for f, s, _, sm, rp, y, yr in packages + ) + media_type = request.accepted_renderer.media_type + + if media_type == PYPI_SIMPLE_V1_JSON: + detail_data = write_simple_detail_json(name, releases) + headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)} + return Response(detail_data, headers=headers) + else: + detail_data = write_simple_detail(name, releases, streamed=True) + kwargs = {"content_type": media_type} + return StreamingHttpResponse(detail_data, kwargs) @extend_schema( request=PackageUploadSerializer, diff --git a/pulp_python/app/tasks/publish.py b/pulp_python/app/tasks/publish.py index 3ab44501..82eec23d 100644 --- a/pulp_python/app/tasks/publish.py +++ b/pulp_python/app/tasks/publish.py @@ -118,6 +118,7 @@ def write_project_page(name, simple_dir, package_releases, publication): metadata_relative_path = f"{project_dir}index.html" with open(metadata_relative_path, "w") as simple_metadata: + # todo? simple_metadata.write(write_simple_detail(name, package_releases)) project_metadata = models.PublishedMetadata.create_from_file( diff --git a/pulp_python/app/utils.py b/pulp_python/app/utils.py index 533caba8..22791f38 100644 --- a/pulp_python/app/utils.py +++ b/pulp_python/app/utils.py @@ -12,10 +12,13 @@ from pulpcore.plugin.models import Remote +# todo: why upper case? PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL" """TODO This serial constant is temporary until Python repositories implements serials""" PYPI_SERIAL_CONSTANT = 1000000000 +PYPI_API_VERSION = "1.0" + simple_index_template = """ @@ -38,8 +41,8 @@

Links for {{ project_name }}

- {% for name, path, sha256 in project_packages %} - {{ name }}
+ {% for pkg in project_packages %} + {{ pkg.filename }}
{% endfor %} @@ -158,6 +161,9 @@ def parse_metadata(project, version, distribution): package["requires_python"] = distribution.get("requires_python") or package.get( "requires_python" ) # noqa: E501 + package["yanked"] = distribution.get("yanked") or False + package["yanked_reason"] = distribution.get("yanked_reason") or "" + package["sha256_metadata"] = distribution.get("data-dist-info-metadata", {}).get("sha256") or "" return package @@ -203,6 +209,10 @@ def artifact_to_python_content_data(filename, artifact, domain=None): data["filename"] = filename data["pulp_domain"] = domain or artifact.pulp_domain data["_pulp_domain"] = data["pulp_domain"] + # todo: how to get these / should they be here? + # data["yanked"] = False + # data["yanked_reason"] = "" + # data["sha256_metadata"] = "" return data @@ -325,6 +335,7 @@ def python_content_to_info(content): "platform": content.platform or "", "requires_dist": json_to_dict(content.requires_dist) or None, "classifiers": json_to_dict(content.classifiers) or None, + # todo yanked "yanked": False, # These are no longer used on PyPI, but are still present "yanked_reason": None, # New core metadata (Version 2.1, 2.2, 2.4) @@ -395,6 +406,7 @@ def find_artifact(): "upload_time": str(content.pulp_created), "upload_time_iso_8601": str(content.pulp_created.isoformat()), "url": url, + # todo yanked "yanked": False, "yanked_reason": None, } @@ -414,6 +426,53 @@ def write_simple_detail(project_name, project_packages, streamed=False): return detail.stream(**context) if streamed else detail.render(**context) +def write_simple_index_json(project_names): + """Writes the simple index in JSON format.""" + return { + "meta": {"api-version": PYPI_API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT}, + "projects": [ + {"name": name, "_last-serial": PYPI_SERIAL_CONSTANT} for name in project_names + ], + } + + +def write_simple_detail_json(project_name, project_packages): + """Writes the simple detail page in JSON format.""" + return { + "meta": {"api-version": PYPI_API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT}, + "name": canonicalize_name(project_name), + "files": [ + { + # v1.0, PEP 691 + "filename": package["filename"], + "url": package["url"], + "hashes": {"sha256": package["sha256"]}, + "requires_python": package["requires_python"] or None, + # data-dist-info-metadata is deprecated alias for core-metadata + "data-dist-info-metadata": ( + {"sha256": package["sha256_metadata"]} if package["sha256_metadata"] else False + ), + "yanked": ( + package["yanked_reason"] + if package["yanked"] and package["yanked_reason"] + else package["yanked"] + ), + # gpg-sig (not in warehouse) + # todo (from new PEPs): + # size (v1.1, PEP 700) + # upload-time (v1.1, PEP 700) + # core-metadata (PEP 7.14) + # provenance (v1.3, PEP 740) + } + for package in project_packages + ], + # todo (from new PEPs): + # versions (v1.1, PEP 700) + # alternate-locations (v1.2, PEP 708) + # project-status (v1.4, PEP 792 - pypi and docs differ) + } + + class PackageIncludeFilter: """A special class to help filter Package's based on a remote's include/exclude""" diff --git a/pulp_python/tests/functional/api/test_full_mirror.py b/pulp_python/tests/functional/api/test_full_mirror.py index b2e9b404..c4137b5a 100644 --- a/pulp_python/tests/functional/api/test_full_mirror.py +++ b/pulp_python/tests/functional/api/test_full_mirror.py @@ -66,7 +66,7 @@ def test_pull_through_filter(python_remote_factory, python_distribution_factory) r = requests.get(f"{distro.base_url}simple/pulpcore/") assert r.status_code == 404 - assert r.json() == {"detail": "pulpcore does not exist."} + assert r.text == "404 Not Found" r = requests.get(f"{distro.base_url}simple/shelf-reader/") assert r.status_code == 200 @@ -86,7 +86,7 @@ def test_pull_through_filter(python_remote_factory, python_distribution_factory) r = requests.get(f"{distro.base_url}simple/django/") assert r.status_code == 404 - assert r.json() == {"detail": "django does not exist."} + assert r.text == "404 Not Found" r = requests.get(f"{distro.base_url}simple/pulpcore/") assert r.status_code == 502 diff --git a/pulp_python/tests/functional/api/test_pypi_simple_json_api.py b/pulp_python/tests/functional/api/test_pypi_simple_json_api.py new file mode 100644 index 00000000..befa2ae3 --- /dev/null +++ b/pulp_python/tests/functional/api/test_pypi_simple_json_api.py @@ -0,0 +1,98 @@ +from urllib.parse import urljoin + +import pytest +import requests + +from pulp_python.tests.functional.constants import PYTHON_SM_PROJECT_SPECIFIER + +API_VERSION = "1.0" +PYPI_SERIAL_CONSTANT = 1000000000 + +PYPI_TEXT_HTML = "text/html" +PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html" +PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json" + + +@pytest.mark.parallel +def test_simple_json_index_api( + python_remote_factory, python_repo_with_sync, python_distribution_factory +): + remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER) + repo = python_repo_with_sync(remote) + distro = python_distribution_factory(repository=repo) + + url = urljoin(distro.base_url, "simple/") + headers = {"Accept": PYPI_SIMPLE_V1_JSON} + + response = requests.get(url, headers=headers) + assert response.headers["Content-Type"] == PYPI_SIMPLE_V1_JSON + assert response.headers["X-PyPI-Last-Serial"] == str(PYPI_SERIAL_CONSTANT) + + data = response.json() + assert data["meta"] == {"api-version": API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT} + assert data["projects"] + for project in data["projects"]: + for i in ["_last-serial", "name"]: + assert i in project + + +@pytest.mark.parallel +def test_simple_json_detail_api( + python_remote_factory, python_repo_with_sync, python_distribution_factory +): + remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER) + repo = python_repo_with_sync(remote) + distro = python_distribution_factory(repository=repo) + + url = f'{urljoin(distro.base_url, "simple/")}aiohttp' + headers = {"Accept": PYPI_SIMPLE_V1_JSON} + + response = requests.get(url, headers=headers) + assert response.headers["Content-Type"] == PYPI_SIMPLE_V1_JSON + assert response.headers["X-PyPI-Last-Serial"] == str(PYPI_SERIAL_CONSTANT) + + data = response.json() + assert data["meta"] == {"api-version": API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT} + assert data["name"] == "aiohttp" + assert data["files"] + for file in data["files"]: + for i in [ + "filename", + "url", + "hashes", + "data-dist-info-metadata", + "requires_python", + "yanked", + ]: + assert i in file + + +@pytest.mark.parallel +@pytest.mark.parametrize( + "header, result", + [ + (PYPI_TEXT_HTML, PYPI_TEXT_HTML), + (PYPI_SIMPLE_V1_HTML, PYPI_SIMPLE_V1_HTML), + (PYPI_SIMPLE_V1_JSON, PYPI_SIMPLE_V1_JSON), + # Follows defined ordering (html, pypi html, pypi json) + (f"{PYPI_SIMPLE_V1_JSON}, {PYPI_SIMPLE_V1_HTML}", PYPI_SIMPLE_V1_HTML), + # Everything else should be html + ("", PYPI_TEXT_HTML), + ("application/json", PYPI_TEXT_HTML), + ("sth/else", PYPI_TEXT_HTML), + ], +) +def test_simple_api_content_headers( + python_remote_factory, python_repo_with_sync, python_distribution_factory, header, result +): + remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER) + repo = python_repo_with_sync(remote) + distro = python_distribution_factory(repository=repo) + + index_url = urljoin(distro.base_url, "simple/") + detail_url = f"{index_url}aiohttp" + + for url in [index_url, detail_url]: + response = requests.get(url, headers={"Accept": header}) + assert response.status_code == 200 + assert result in response.headers["Content-Type"] From a684757701edd5dec41fe89f57f12b031bb7c269 Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Fri, 31 Oct 2025 12:34:41 -0400 Subject: [PATCH 2/2] temp --- ...packagecontent_sha256_metadata_and_more.py | 4 +- pulp_python/app/models.py | 2 +- pulp_python/app/pypi/views.py | 4 +- pulp_python/app/tasks/publish.py | 1 - pulp_python/app/utils.py | 38 +++++++++---------- 5 files changed, 22 insertions(+), 27 deletions(-) diff --git a/pulp_python/app/migrations/0016_pythonpackagecontent_sha256_metadata_and_more.py b/pulp_python/app/migrations/0016_pythonpackagecontent_sha256_metadata_and_more.py index 119d5fd4..e43a7b41 100644 --- a/pulp_python/app/migrations/0016_pythonpackagecontent_sha256_metadata_and_more.py +++ b/pulp_python/app/migrations/0016_pythonpackagecontent_sha256_metadata_and_more.py @@ -12,8 +12,8 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name="pythonpackagecontent", - name="sha256_metadata", - field=models.CharField(default="", max_length=64), + name="metadata_sha256", + field=models.CharField(null=True, max_length=64), preserve_default=False, ), migrations.AddField( diff --git a/pulp_python/app/models.py b/pulp_python/app/models.py index eab7012d..3649134f 100644 --- a/pulp_python/app/models.py +++ b/pulp_python/app/models.py @@ -192,7 +192,7 @@ class PythonPackageContent(Content): packagetype = models.TextField(choices=PACKAGE_TYPES) python_version = models.TextField() sha256 = models.CharField(db_index=True, max_length=64) - sha256_metadata = models.CharField(max_length=64) + metadata_sha256 = models.CharField(max_length=64, null=True) yanked = models.BooleanField(default=False) yanked_reason = models.TextField() diff --git a/pulp_python/app/pypi/views.py b/pulp_python/app/pypi/views.py index b1301139..c68f6ecb 100644 --- a/pulp_python/app/pypi/views.py +++ b/pulp_python/app/pypi/views.py @@ -342,7 +342,7 @@ def retrieve(self, request, path, package): "filename", "sha256", "name", - "sha256_metadata", + "metadata_sha256", "requires_python", "yanked", "yanked_reason", @@ -361,7 +361,7 @@ def retrieve(self, request, path, package): "filename": f, "url": urljoin(self.base_content_url, f"{path}/{f}"), "sha256": s, - "sha256_metadata": sm, + "metadata_sha256": sm, "requires_python": rp, "yanked": y, "yanked_reason": yr, diff --git a/pulp_python/app/tasks/publish.py b/pulp_python/app/tasks/publish.py index 82eec23d..3ab44501 100644 --- a/pulp_python/app/tasks/publish.py +++ b/pulp_python/app/tasks/publish.py @@ -118,7 +118,6 @@ def write_project_page(name, simple_dir, package_releases, publication): metadata_relative_path = f"{project_dir}index.html" with open(metadata_relative_path, "w") as simple_metadata: - # todo? simple_metadata.write(write_simple_detail(name, package_releases)) project_metadata = models.PublishedMetadata.create_from_file( diff --git a/pulp_python/app/utils.py b/pulp_python/app/utils.py index 22791f38..879ec65b 100644 --- a/pulp_python/app/utils.py +++ b/pulp_python/app/utils.py @@ -12,18 +12,17 @@ from pulpcore.plugin.models import Remote -# todo: why upper case? PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL" """TODO This serial constant is temporary until Python repositories implements serials""" PYPI_SERIAL_CONSTANT = 1000000000 -PYPI_API_VERSION = "1.0" +SIMPLE_API_VERSION = "1.0" simple_index_template = """ Simple Index - + {% for name, canonical_name in projects %} @@ -33,16 +32,17 @@ """ +# noqa: E501 simple_detail_template = """ Links for {{ project_name }} - +

Links for {{ project_name }}

{% for pkg in project_packages %} - {{ pkg.filename }}
+ {{ pkg.filename }}{% if pkg.yanked %}data-yanked="{{ pkg.yanked_reason }}"{% endif %}
{% endfor %} @@ -131,6 +131,9 @@ def parse_project_metadata(project): # Release metadata "packagetype": project.get("packagetype") or "", "python_version": project.get("python_version") or "", + "yanked": False, + "yanked_reason": "", + "metadata_sha256": "", # TODO } @@ -163,7 +166,7 @@ def parse_metadata(project, version, distribution): ) # noqa: E501 package["yanked"] = distribution.get("yanked") or False package["yanked_reason"] = distribution.get("yanked_reason") or "" - package["sha256_metadata"] = distribution.get("data-dist-info-metadata", {}).get("sha256") or "" + package["metadata_sha256"] = distribution.get("data-dist-info-metadata", {}).get("sha256") or "" return package @@ -209,10 +212,6 @@ def artifact_to_python_content_data(filename, artifact, domain=None): data["filename"] = filename data["pulp_domain"] = domain or artifact.pulp_domain data["_pulp_domain"] = data["pulp_domain"] - # todo: how to get these / should they be here? - # data["yanked"] = False - # data["yanked_reason"] = "" - # data["sha256_metadata"] = "" return data @@ -335,7 +334,6 @@ def python_content_to_info(content): "platform": content.platform or "", "requires_dist": json_to_dict(content.requires_dist) or None, "classifiers": json_to_dict(content.classifiers) or None, - # todo yanked "yanked": False, # These are no longer used on PyPI, but are still present "yanked_reason": None, # New core metadata (Version 2.1, 2.2, 2.4) @@ -429,7 +427,7 @@ def write_simple_detail(project_name, project_packages, streamed=False): def write_simple_index_json(project_names): """Writes the simple index in JSON format.""" return { - "meta": {"api-version": PYPI_API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT}, + "meta": {"api-version": SIMPLE_API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT}, "projects": [ {"name": name, "_last-serial": PYPI_SERIAL_CONSTANT} for name in project_names ], @@ -439,7 +437,7 @@ def write_simple_index_json(project_names): def write_simple_detail_json(project_name, project_packages): """Writes the simple detail page in JSON format.""" return { - "meta": {"api-version": PYPI_API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT}, + "meta": {"api-version": SIMPLE_API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT}, "name": canonicalize_name(project_name), "files": [ { @@ -450,23 +448,21 @@ def write_simple_detail_json(project_name, project_packages): "requires_python": package["requires_python"] or None, # data-dist-info-metadata is deprecated alias for core-metadata "data-dist-info-metadata": ( - {"sha256": package["sha256_metadata"]} if package["sha256_metadata"] else False + {"sha256": package["metadata_sha256"]} if package["metadata_sha256"] else False ), "yanked": ( package["yanked_reason"] if package["yanked"] and package["yanked_reason"] else package["yanked"] ), - # gpg-sig (not in warehouse) - # todo (from new PEPs): - # size (v1.1, PEP 700) - # upload-time (v1.1, PEP 700) - # core-metadata (PEP 7.14) - # provenance (v1.3, PEP 740) + # TODO: + # size, upload-time (v1.1, PEP 700) + # core-metadata (PEP 714) + # provenance and digital attestation (v1.3, PEP 740) } for package in project_packages ], - # todo (from new PEPs): + # TODO: # versions (v1.1, PEP 700) # alternate-locations (v1.2, PEP 708) # project-status (v1.4, PEP 792 - pypi and docs differ)