Repository: pypi/warehouse

Issue:
Redirect files &/or display action URLs from legacy
I see some users using legacy URLs like:

* https://pypi.python.org/pypi?:action=files&name=limix&version=1.0.14 (on legacy: https://legacy.pypi.org/pypi?:action=files&name=limix&version=1.0.14 )
* https://pypi.python.org/pypi?:action=display&name=craft-ai (on legacy: https://legacy.pypi.org/pypi?:action=display&name=craft-ai )
* https://pypi.python.org/pypi?:action=display&name=text_progress_bar (on legacy: https://legacy.pypi.org/pypi?:action=display&name=text_progress_bar )

It'd be nice to redirect the display URLs and, potentially, the files URLs to Warehouse.

Followup to #3799.


Discussion:
Working on this.

Resolving patch:
diff --git a/warehouse/legacy/api/pypi.py b/warehouse/legacy/api/pypi.py
--- a/warehouse/legacy/api/pypi.py
+++ b/warehouse/legacy/api/pypi.py
@@ -118,3 +118,39 @@ def browse(request):
             'search', _query={'c': classifier.classifier}
         )
     )
+
+
+@view_config(route_name='legacy.api.pypi.files')
+def files(request):
+    name = request.params.get('name')
+    version = request.params.get('version')
+
+    if (not name) or (not version):
+        raise HTTPNotFound
+
+    return HTTPMovedPermanently(
+        request.route_path(
+            'packaging.release', name=name, version=version, _anchor="files"
+        )
+    )
+
+
+@view_config(route_name='legacy.api.pypi.display')
+def display(request):
+    name = request.params.get('name')
+    version = request.params.get('version')
+
+    if not name:
+        raise HTTPNotFound
+
+    if version:
+        return HTTPMovedPermanently(
+            request.route_path(
+                'packaging.release', name=name, version=version
+            )
+        )
+    return HTTPMovedPermanently(
+        request.route_path(
+            'packaging.project', name=name
+        )
+    )
diff --git a/warehouse/routes.py b/warehouse/routes.py
--- a/warehouse/routes.py
+++ b/warehouse/routes.py
@@ -289,6 +289,16 @@ def includeme(config):
         'browse',
         domain=warehouse,
     )
+    config.add_pypi_action_route(
+        'legacy.api.pypi.files',
+        'files',
+        domain=warehouse,
+    )
+    config.add_pypi_action_route(
+        'legacy.api.pypi.display',
+        'display',
+        domain=warehouse,
+    )
 
     # Legacy XMLRPC
     config.add_xmlrpc_endpoint(


Test patch:
diff --git a/tests/unit/legacy/api/test_pypi.py b/tests/unit/legacy/api/test_pypi.py
--- a/tests/unit/legacy/api/test_pypi.py
+++ b/tests/unit/legacy/api/test_pypi.py
@@ -131,3 +131,105 @@ def test_browse_bad_id(self, db_request):
 
         with pytest.raises(HTTPNotFound):
             pypi.browse(db_request)
+
+
+class TestFiles:
+
+    def test_files(self, db_request):
+        name = "pip"
+        version = "10.0.0"
+
+        db_request.params = {"name": name, "version": version}
+        db_request.route_path = pretend.call_recorder(
+            lambda *a, **kw: f'/project/{name}/{version}/#files'
+        )
+
+        result = pypi.files(db_request)
+
+        assert isinstance(result, HTTPMovedPermanently)
+        assert result.headers['Location'] == (
+            f'/project/{name}/{version}/#files'
+        )
+        assert result.status_code == 301
+        assert db_request.route_path.calls == [
+            pretend.call(
+                'packaging.release',
+                name=name,
+                version=version,
+                _anchor="files"
+            )
+        ]
+
+    def test_files_no_version(self, db_request):
+        name = "pip"
+
+        db_request.params = {"name": name}
+
+        with pytest.raises(HTTPNotFound):
+            pypi.files(db_request)
+
+    def test_files_no_name(self, db_request):
+        version = "10.0.0"
+
+        db_request.params = {"version": version}
+
+        with pytest.raises(HTTPNotFound):
+            pypi.files(db_request)
+
+
+class TestDisplay:
+
+    def test_display(self, db_request):
+        name = "pip"
+        version = "10.0.0"
+
+        db_request.params = {"name": name, "version": version}
+        db_request.route_path = pretend.call_recorder(
+            lambda *a, **kw: f'/project/{name}/{version}/'
+        )
+
+        result = pypi.display(db_request)
+
+        assert isinstance(result, HTTPMovedPermanently)
+        assert result.headers['Location'] == (
+            f'/project/{name}/{version}/'
+        )
+        assert result.status_code == 301
+        assert db_request.route_path.calls == [
+            pretend.call(
+                'packaging.release',
+                name=name,
+                version=version,
+            )
+        ]
+
+    def test_display_no_version(self, db_request):
+        name = "pip"
+
+        db_request.params = {"name": name}
+
+        db_request.route_path = pretend.call_recorder(
+            lambda *a, **kw: f'/project/{name}/'
+        )
+
+        result = pypi.display(db_request)
+
+        assert isinstance(result, HTTPMovedPermanently)
+        assert result.headers['Location'] == (
+            f'/project/{name}/'
+        )
+        assert result.status_code == 301
+        assert db_request.route_path.calls == [
+            pretend.call(
+                'packaging.project',
+                name=name,
+            )
+        ]
+
+    def test_display_no_name(self, db_request):
+        version = "10.0.0"
+
+        db_request.params = {"version": version}
+
+        with pytest.raises(HTTPNotFound):
+            pypi.display(db_request)
diff --git a/tests/unit/test_routes.py b/tests/unit/test_routes.py
--- a/tests/unit/test_routes.py
+++ b/tests/unit/test_routes.py
@@ -332,6 +332,16 @@ def add_policy(name, filename):
             'browse',
             domain=warehouse,
         ),
+        pretend.call(
+            'legacy.api.pypi.files',
+            'files',
+            domain=warehouse,
+        ),
+        pretend.call(
+            'legacy.api.pypi.display',
+            'display',
+            domain=warehouse,
+        ),
     ]
 
     assert config.add_pypi_action_redirect.calls == [