Repository: mercadona/rele
Pull request #148 (2020-01-08 16:21:47)

 Allow multiple filters
### :tophat: What?

Modify subscription constructor to allow iterables in `filter_by` and apply all filters.
 
### :thinking: Why?

Current version only allows one filter.

### :link: Related issue

Solver #134 


jonasae: > What do you think about documentation? I already started with the issue, maybe you find worthy next chunk.
> 
> ```diff
> diff --git a/rele/subscription.py b/rele/subscription.py
> index 73f6b03..3c4c276 100644
> --- a/rele/subscription.py
> +++ b/rele/subscription.py
> @@ -139,9 +139,9 @@ def sub(topic, prefix=None, suffix=None, filter_by=None):
>      :param suffix: string An optional suffix to the subscription name.
>                     Useful when you have two subscribers in the same project
>                     that are subscribed to the same topic.
> -    :param filter_by: function An optional function that
> -                      filters the messages to be processed
> -                      by the sub regarding their attributes.
> +    :param filter_by: Union[function, list] An optional function or tuple of
> +                      functions that filters the messages to be processed by
> +                      the sub regarding their attributes.
>      :return: :class:`~rele.subscription.Subscription`
>      """
>  
>  
> ```

Sure. I realised the documentation should be updated when I was about to finish.
Should I update de docs first in a separated PR or do it in this one?

Patch:
diff --git a/rele/config.py b/rele/config.py
index 29a8581..0bf6496 100644
--- a/rele/config.py
+++ b/rele/config.py
@@ -66,7 +66,7 @@ def load_subscriptions_from_paths(sub_module_paths, sub_prefix=None, filter_by=N
                     attribute.set_prefix(sub_prefix)
 
                 if filter_by and not attribute.filter_by:
-                    attribute.set_filter_by(filter_by)
+                    attribute.set_filters(filter_by)
 
                 subscriptions.append(attribute)
     return subscriptions
diff --git a/rele/subscription.py b/rele/subscription.py
index 73f6b03..f5c87b5 100644
--- a/rele/subscription.py
+++ b/rele/subscription.py
@@ -17,7 +17,15 @@ class Subscription:
         self.topic = topic
         self._prefix = prefix
         self._suffix = suffix
-        self._filter_by = filter_by
+        self._filters = self._init_filters(filter_by)
+
+    def _init_filters(self, filter_by):
+        if hasattr(filter_by, "__iter__"):
+            return filter_by
+        elif filter_by:
+            return [filter_by]
+
+        return None
 
     @property
     def name(self):
@@ -33,16 +41,16 @@ class Subscription:
 
     @property
     def filter_by(self):
-        return self._filter_by
+        return self._filters
 
-    def set_filter_by(self, filter_by):
-        self._filter_by = filter_by
+    def set_filters(self, filter_by):
+        self._filters = filter_by
 
     def __call__(self, data, **kwargs):
         if "published_at" in kwargs:
             kwargs["published_at"] = float(kwargs["published_at"])
 
-        if self._filter_returns_false(kwargs):
+        if self._any_filter_returns_false(kwargs):
             return
 
         return self._func(data, **kwargs)
@@ -50,8 +58,11 @@ class Subscription:
     def __str__(self):
         return f"{self.name} - {self._func.__name__}"
 
-    def _filter_returns_false(self, kwargs):
-        return self._filter_by and not self._filter_by(kwargs)
+    def _any_filter_returns_false(self, kwargs):
+        if not self._filters:
+            return False
+
+        return not all(filter(kwargs) for filter in self._filters)
 
 
 class Callback:
@@ -139,9 +150,9 @@ def sub(topic, prefix=None, suffix=None, filter_by=None):
     :param suffix: string An optional suffix to the subscription name.
                    Useful when you have two subscribers in the same project
                    that are subscribed to the same topic.
-    :param filter_by: function An optional function that
-                      filters the messages to be processed
-                      by the sub regarding their attributes.
+    :param filter_by: Union[function, list] An optional function or tuple of
+                      functions that filters the messages to be processed by
+                      the sub regarding their attributes.
     :return: :class:`~rele.subscription.Subscription`
     """
 


Test patch:
diff --git a/tests/test_subscription.py b/tests/test_subscription.py
index d3e07d5..1cf87c5 100644
--- a/tests/test_subscription.py
+++ b/tests/test_subscription.py
@@ -35,11 +35,20 @@ def landscape_filter(kwargs):
     return kwargs.get("type") == "landscape"
 
 
+def gif_filter(kwargs):
+    return kwargs.get("format") == "gif"
+
+
 @sub(topic="photo-updated", filter_by=landscape_filter)
 def sub_process_landscape_photos(data, **kwargs):
     return f'Received a photo of type {kwargs.get("type")}'
 
 
+@sub(topic="photo-updated", filter_by=[landscape_filter, gif_filter])
+def sub_process_landscape_gif_photos(data, **kwargs):
+    return f'Received a {kwargs.get("format")} photo of type {kwargs.get("type")}'
+
+
 class TestSubscription:
     def test_subs_return_subscription_objects(self):
         assert isinstance(sub_stub, Subscription)
@@ -70,6 +79,33 @@ class TestSubscription:
 
         assert response is None
 
+    def test_sub_executes_when_message_attributes_matches_multiple_criterias(self):
+        data = {"name": "my_new_photo.jpeg"}
+        response = sub_process_landscape_gif_photos(
+            data, type="landscape", format="gif"
+        )
+
+        assert response == "Received a gif photo of type landscape"
+
+    @pytest.mark.parametrize(
+        "type, format",
+        [
+            ("portrait", "gif"),
+            ("landscape", "jpg"),
+            ("portrait", "jpg"),
+            (None, "gif"),
+            ("portrait", None),
+            (None, None),
+        ],
+    )
+    def test_sub_is_not_executed_when_message_attribs_dont_match_all_criterias(
+        self, type, format
+    ):
+        data = {"name": "my_new_photo.jpeg"}
+        response = sub_process_landscape_gif_photos(data, type=type, format=format)
+
+        assert response is None
+
 
 class TestCallback:
     @pytest.fixture(autouse=True)