Repository: PrefectHQ/prefect

Issue:
Create RSS Tasks for Task Library



Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -17,6 +17,7 @@
         "google-cloud-storage >= 1.13, < 2.0",
     ],
     "kubernetes": ["dask-kubernetes == 0.7.0", "kubernetes >= 8.0.1, < 9.0"],
+    "rss": ["feedparser >= 5.0.1, < 6.0"],
     "templates": ["jinja2 >= 2.0, < 3.0"],
     "viz": ["graphviz >= 0.8.3"],
     "twitter": ["tweepy >= 3.5, < 4.0"],
diff --git a/src/prefect/tasks/rss/__init__.py b/src/prefect/tasks/rss/__init__.py
new file mode 100644
--- /dev/null
+++ b/src/prefect/tasks/rss/__init__.py
@@ -0,0 +1,9 @@
+"""
+Tasks for interacting with RSS feeds.
+"""
+try:
+    from prefect.tasks.rss.feed import ParseRSSFeed
+except ImportError:
+    raise ImportError(
+        'Using `prefect.tasks.rss` requires Prefect to be installed with the "rss" extra.'
+    )
diff --git a/src/prefect/tasks/rss/feed.py b/src/prefect/tasks/rss/feed.py
new file mode 100644
--- /dev/null
+++ b/src/prefect/tasks/rss/feed.py
@@ -0,0 +1,42 @@
+from typing import Any
+
+import feedparser
+
+from prefect import Task
+from prefect.utilities.tasks import defaults_from_attrs
+
+
+class ParseRSSFeed(Task):
+    """
+    Task for parsing RSS feeds.
+
+    Args:
+        - feed_url (str): A remote URL pointing to an RSS feed
+        - **kwargs (dict, optional): additional keyword arguments to pass to the Task
+            constructor
+    """
+
+    def __init__(self, feed_url: str = None, **kwargs: Any):
+        self.feed_url = feed_url
+
+        super().__init__(**kwargs)
+
+    @defaults_from_attrs("feed_url")
+    def run(self, feed_url: str = None) -> "feedparser.FeedParserDict":
+        """
+        Task run method.
+
+        Args:
+            - feed_url (str): A remote URL pointing to an RSS feed
+
+        Return:
+            - FeedParserDict: A dictionary representing the information from the
+                parsed feed. The object is accessable through indexing and attributes.
+
+        Raises:
+            - ValueError: if `feed_url` is `None`
+        """
+        if not feed_url:
+            raise ValueError("The feed_url must be provided.")
+
+        return feedparser.parse(feed_url)


Test patch:
diff --git a/tests/tasks/rss/__init__.py b/tests/tasks/rss/__init__.py
new file mode 100644
--- /dev/null
+++ b/tests/tasks/rss/__init__.py
@@ -0,0 +1,3 @@
+import pytest
+
+pytest.importorskip("feedparser")
diff --git a/tests/tasks/rss/test_feed.py b/tests/tasks/rss/test_feed.py
new file mode 100644
--- /dev/null
+++ b/tests/tasks/rss/test_feed.py
@@ -0,0 +1,52 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from prefect.tasks.rss import ParseRSSFeed
+
+
+class TestParseRSSFeedTask:
+    def test_empty_initialization(self):
+        task = ParseRSSFeed()
+        assert not task.feed_url
+
+    def test_filled_initialization(self):
+        task = ParseRSSFeed(feed_url="test")
+        assert task.feed_url == "test"
+
+    def test_empty_feed_url_raises_error(self):
+        task = ParseRSSFeed()
+        with pytest.raises(ValueError):
+            task.run()
+
+    def test_invalid_feed_url_raises_error(self):
+        task = ParseRSSFeed()
+        with pytest.raises(ValueError):
+            task.run(feed_url=None)
+
+    def test_feed_url_init_value_is_used(self, monkeypatch):
+        task = ParseRSSFeed(feed_url="test")
+
+        parse = MagicMock()
+        monkeypatch.setattr("prefect.tasks.rss.feed.feedparser.parse", parse)
+
+        task.run()
+        assert parse.call_args[0][0] == "test"
+
+    def test_feed_url_run_value_is_used(self, monkeypatch):
+        task = ParseRSSFeed()
+
+        parse = MagicMock()
+        monkeypatch.setattr("prefect.tasks.rss.feed.feedparser.parse", parse)
+
+        task.run(feed_url="test")
+        assert parse.call_args[0][0] == "test"
+
+    def test_feed_url_value_is_replaced(self, monkeypatch):
+        task = ParseRSSFeed(feed_url="test")
+
+        parse = MagicMock()
+        monkeypatch.setattr("prefect.tasks.rss.feed.feedparser.parse", parse)
+
+        task.run(feed_url="b_test")
+        assert parse.call_args[0][0] == "b_test"