Repository: pypi/warehouse

Issue:
Package update feeds
Was just having a thought about a neat feature that would be really helpful for me and probably others. Thinking of downstream/secondary package maintainers for instance though others may find it valuable too. My primary personal use case is helping keep [conda-forge]( http://conda-forge.github.io/ ) up to date. Though maybe Linux packagers and others would be interested

It would be really neat to have some kind of feed that would be published to when a particular package is updated. Mainly interested in version bumps, but potentially other events could be valuable if people are interested. It would be good if this feed was designed so that a person or bot could subscribe ideally without an account. Also it might be nice to have some way to filter out dev/prereleases from stable releases.


Discussion:
This is definitely one of those features I would expect from a package index. Right now I keep track of updates by subscribing to the `releases` feeds of the repo's of the projects I'm interested in.
That's certainly another option. Though not everyone is great about keeping their releases (or tags) in sync with PyPI releases. That said, I expect we will use a variety of different sources.
Another use-case for this: being able to track when new files are uploaded to an existing package version (eg additional wheel variants being uploaded) - so tools like pyup.io can know to update the requirements files with the additional hashes.
I figure this would be based on the same mechanism that could send out notification emails, as requested in #98.
Thanks for your note and sorry for the slow response!

The folks working on Warehouse have gotten [funding to concentrate on improving and deploying Warehouse](https://pyfound.blogspot.com/2017/11/the-psf-awarded-moss-grant-pypi.html), and have kicked off work towards [our development roadmap](https://wiki.python.org/psf/WarehouseRoadmap) -- the most urgent task is to improve Warehouse to the point where we can redirect pypi.python.org to pypi.org so the site is more sustainable and reliable. Since this feature isn't something that the legacy site has, I've moved it to a future milestone.

Thanks and sorry again for the wait.
Folks who need this might want to check whether the Libraries.io API for https://libraries.io/pypi might suit their needs in the short term.
Here's a request for RSS feeds specifically: https://github.com/pypa/warehouse/issues/5612

Resolving patch:
diff --git a/warehouse/routes.py b/warehouse/routes.py
--- a/warehouse/routes.py
+++ b/warehouse/routes.py
@@ -302,6 +302,14 @@ def includeme(config):
     # RSS
     config.add_route("rss.updates", "/rss/updates.xml", domain=warehouse)
     config.add_route("rss.packages", "/rss/packages.xml", domain=warehouse)
+    config.add_route(
+        "rss.project.releases",
+        "/rss/project/{name}/releases.xml",
+        factory="warehouse.packaging.models:ProjectFactory",
+        traverse="/{name}/",
+        read_only=True,
+        domain=warehouse,
+    )
 
     # Legacy URLs
     config.add_route("legacy.api.simple.index", "/simple/", domain=warehouse)
diff --git a/warehouse/rss/views.py b/warehouse/rss/views.py
--- a/warehouse/rss/views.py
+++ b/warehouse/rss/views.py
@@ -104,3 +104,33 @@ def rss_packages(request):
     ]
 
     return {"newest_projects": tuple(zip(newest_projects, project_authors))}
+
+
+@view_config(
+    route_name="rss.project.releases",
+    context=Project,
+    renderer="rss/project_releases.xml",
+    decorator=[
+        origin_cache(
+            1 * 24 * 60 * 60, stale_if_error=5 * 24 * 60 * 60  # 1 day, 5 days stale
+        )
+    ],
+)
+def rss_project_releases(project, request):
+    request.response.content_type = "text/xml"
+
+    request.find_service(name="csp").merge(XML_CSP)
+
+    latest_releases = (
+        request.db.query(Release)
+        .filter(Release.project == project)
+        .order_by(Release.created.desc())
+        .limit(40)
+        .all()
+    )
+    release_authors = [_format_author(release) for release in latest_releases]
+
+    return {
+        "project": project,
+        "latest_releases": tuple(zip(latest_releases, release_authors)),
+    }


Test patch:
diff --git a/tests/unit/rss/test_views.py b/tests/unit/rss/test_views.py
--- a/tests/unit/rss/test_views.py
+++ b/tests/unit/rss/test_views.py
@@ -73,6 +73,35 @@ def test_rss_packages(db_request):
     assert db_request.response.content_type == "text/xml"
 
 
+def test_rss_project_releases(db_request):
+    db_request.find_service = pretend.call_recorder(
+        lambda *args, **kwargs: pretend.stub(
+            enabled=False, csp_policy=pretend.stub(), merge=lambda _: None
+        )
+    )
+
+    db_request.session = pretend.stub()
+
+    project = ProjectFactory.create()
+
+    release_v1 = ReleaseFactory.create(project=project, version="1.0.0")
+    release_v1.created = datetime.date(2018, 1, 1)
+    release_v3 = ReleaseFactory.create(project=project, version="3.0.0")
+    release_v3.created = datetime.date(2019, 1, 1)
+    release_v2 = ReleaseFactory.create(project=project, version="2.0.0")
+    release_v2.created = datetime.date(2020, 1, 1)
+
+    release_v3.author_email = "noreply@pypi.org"
+
+    assert rss.rss_project_releases(project, db_request) == {
+        "project": project,
+        "latest_releases": tuple(
+            zip((release_v2, release_v3, release_v1), (None, "noreply@pypi.org", None))
+        ),
+    }
+    assert db_request.response.content_type == "text/xml"
+
+
 def test_format_author(db_request):
     db_request.find_service = pretend.call_recorder(
         lambda *args, **kwargs: pretend.stub(
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
@@ -303,6 +303,14 @@ def add_policy(name, filename):
         pretend.call("ses.hook", "/_/ses-hook/", domain=warehouse),
         pretend.call("rss.updates", "/rss/updates.xml", domain=warehouse),
         pretend.call("rss.packages", "/rss/packages.xml", domain=warehouse),
+        pretend.call(
+            "rss.project.releases",
+            "/rss/project/{name}/releases.xml",
+            factory="warehouse.packaging.models:ProjectFactory",
+            traverse="/{name}/",
+            read_only=True,
+            domain=warehouse,
+        ),
         pretend.call("legacy.api.simple.index", "/simple/", domain=warehouse),
         pretend.call(
             "legacy.api.simple.detail",