Repository: pypi/warehouse

Issue:
JSON endpoint for project is 404 if only yanked releases are available
From https://github.com/pypa/warehouse/issues/3709#issuecomment-754973958, it appears that the JSON API for a project is 404 if the only releases available are yanked releases.

E.g. https://pypi.org/pypi/django-twilio2/json should probably look like https://pypi.org/pypi/django-twilio2/0.9.0/json instead of being 404.


Discussion:
@di Do you know if anyone is working on this? I'm interested in taking a crack at it. I'm working through the getting-started and submitting-patches guides, just didn't want to duplicate any efforts.
It does not appear that anyone is working on this at the moment!

Resolving patch:
diff --git a/warehouse/legacy/api/json.py b/warehouse/legacy/api/json.py
--- a/warehouse/legacy/api/json.py
+++ b/warehouse/legacy/api/json.py
@@ -65,8 +65,12 @@ def json_project(project, request):
     try:
         release = (
             request.db.query(Release)
-            .filter(Release.project == project, Release.yanked.is_(False))
-            .order_by(Release.is_prerelease.nullslast(), Release._pypi_ordering.desc())
+            .filter(Release.project == project)
+            .order_by(
+                Release.yanked.asc(),
+                Release.is_prerelease.nullslast(),
+                Release._pypi_ordering.desc(),
+            )
             .limit(1)
             .one()
         )


Test patch:
diff --git a/tests/unit/legacy/api/test_json.py b/tests/unit/legacy/api/test_json.py
--- a/tests/unit/legacy/api/test_json.py
+++ b/tests/unit/legacy/api/test_json.py
@@ -118,6 +118,76 @@ def test_only_prereleases(self, monkeypatch, db_request):
         assert resp is response
         assert json_release.calls == [pretend.call(release, db_request)]
 
+    def test_all_releases_yanked(self, monkeypatch, db_request):
+        """
+        If all releases are yanked, the endpoint should return the same release as
+        if none of the releases are yanked.
+        """
+
+        project = ProjectFactory.create()
+
+        ReleaseFactory.create(project=project, version="1.0", yanked=True)
+        ReleaseFactory.create(project=project, version="2.0", yanked=True)
+        ReleaseFactory.create(project=project, version="4.0.dev0", yanked=True)
+
+        release = ReleaseFactory.create(project=project, version="3.0", yanked=True)
+
+        response = pretend.stub()
+        json_release = pretend.call_recorder(lambda ctx, request: response)
+        monkeypatch.setattr(json, "json_release", json_release)
+
+        resp = json.json_project(project, db_request)
+
+        assert resp is response
+        assert json_release.calls == [pretend.call(release, db_request)]
+
+    def test_latest_release_yanked(self, monkeypatch, db_request):
+        """
+        If the latest version is yanked, the endpoint should fall back on the
+        latest non-prerelease version that is not yanked, if one is available.
+        """
+
+        project = ProjectFactory.create()
+
+        ReleaseFactory.create(project=project, version="1.0")
+        ReleaseFactory.create(project=project, version="3.0", yanked=True)
+        ReleaseFactory.create(project=project, version="3.0.dev0")
+
+        release = ReleaseFactory.create(project=project, version="2.0")
+
+        response = pretend.stub()
+        json_release = pretend.call_recorder(lambda ctx, request: response)
+        monkeypatch.setattr(json, "json_release", json_release)
+
+        resp = json.json_project(project, db_request)
+
+        assert resp is response
+        assert json_release.calls == [pretend.call(release, db_request)]
+
+    def test_all_non_prereleases_yanked(self, monkeypatch, db_request):
+        """
+        If all non-prerelease versions are yanked, the endpoint should return the
+        latest prerelease version that is not yanked.
+        """
+
+        project = ProjectFactory.create()
+
+        ReleaseFactory.create(project=project, version="1.0", yanked=True)
+        ReleaseFactory.create(project=project, version="2.0", yanked=True)
+        ReleaseFactory.create(project=project, version="3.0", yanked=True)
+        ReleaseFactory.create(project=project, version="3.0.dev0", yanked=True)
+
+        release = ReleaseFactory.create(project=project, version="2.0.dev0")
+
+        response = pretend.stub()
+        json_release = pretend.call_recorder(lambda ctx, request: response)
+        monkeypatch.setattr(json, "json_release", json_release)
+
+        resp = json.json_project(project, db_request)
+
+        assert resp is response
+        assert json_release.calls == [pretend.call(release, db_request)]
+
 
 class TestJSONProjectSlash:
     def test_normalizing_redirects(self, db_request):