diff --git a/docs/api-reference/feeds.rst b/docs/api-reference/feeds.rst index 77770389f12d..37e8637be76c 100644 --- a/docs/api-reference/feeds.rst +++ b/docs/api-reference/feeds.rst @@ -21,6 +21,11 @@ Available at https://pypi.org/rss/updates.xml, this feed provides the latest newly created releases for individual projects on PyPI, including the project name and description, release version, and a link to the release page. +Accepts an optional ``after`` integer cursor, which limits results to the +releases after the given seconds since the UTC epoch (e.g., the +``timestamp`` method to a ``datetime.datetime`` object). Note that this changes +the ordering of the results from newest->oldest to oldest->newest. + Project Releases Feed --------------------- diff --git a/tests/unit/rss/test_views.py b/tests/unit/rss/test_views.py index 5465b8ad6720..1ab7cbfaba89 100644 --- a/tests/unit/rss/test_views.py +++ b/tests/unit/rss/test_views.py @@ -46,6 +46,15 @@ def test_rss_updates(db_request): } assert db_request.response.content_type == "text/xml" + after_cursor = datetime.datetime( + release1.created.year, release1.created.month, release1.created.day + ) + db_request.params["after"] = after_cursor.timestamp() + assert rss.rss_updates(db_request) == { + "latest_releases": tuple(zip((release2, release3), ("noreply@pypi.org", None))) + } + assert db_request.response.content_type == "text/xml" + def test_rss_packages(db_request): db_request.find_service = pretend.call_recorder( diff --git a/warehouse/rss/views.py b/warehouse/rss/views.py index 7f62c8bd37e6..d4e2ae1e228d 100644 --- a/warehouse/rss/views.py +++ b/warehouse/rss/views.py @@ -10,8 +10,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime from email.utils import getaddresses +from pyramid.httpexceptions import HTTPBadRequest from pyramid.view import view_config from sqlalchemy.orm import joinedload @@ -59,17 +61,34 @@ def _format_author(release): ], ) def rss_updates(request): + + after_timestamp = request.params.get("after") + if after_timestamp: + try: + after_timestamp = datetime.utcfromtimestamp(int(after_timestamp)) + except ValueError: + raise HTTPBadRequest("'after' must be an integer") from None + latest_releases = ( + request.db.query(Release) + .options(joinedload(Release.project)) + .filter(Release.created > after_timestamp) + .order_by(Release.created.asc()) + .limit(100) + .all() + ) + else: + latest_releases = ( + request.db.query(Release) + .options(joinedload(Release.project)) + .order_by(Release.created.desc()) + .limit(100) + .all() + ) + request.response.content_type = "text/xml" request.find_service(name="csp").merge(XML_CSP) - latest_releases = ( - request.db.query(Release) - .options(joinedload(Release.project)) - .order_by(Release.created.desc()) - .limit(40) - .all() - ) release_authors = [_format_author(release) for release in latest_releases] return {"latest_releases": tuple(zip(latest_releases, release_authors))}