Repository: mercadona/rele
Pull request #66 (2019-06-14 15:01:41)

Filter by message attributes
### :tophat: What?

Add a new parameter called `filter_by` to the `sub` decorator that filters the messages to be processed. The filtering is based on the message attributes.

### :thinking: Why?

There are messages sent to a subscription that shouldn't be processed.

### :link: Related issue

Refs #35 


codecov-io: # [Codecov](https://codecov.io/gh/mercadona/rele/pull/66?src=pr&el=h1) Report
> Merging [#66](https://codecov.io/gh/mercadona/rele/pull/66?src=pr&el=desc) into [master](https://codecov.io/gh/mercadona/rele/commit/72b37a36a527bc22032b97ab24da4380f3c8ee4a?src=pr&el=desc) will **increase** coverage by `0.17%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/mercadona/rele/pull/66/graphs/tree.svg?width=650&token=QcryolI2v9&height=150&src=pr)](https://codecov.io/gh/mercadona/rele/pull/66?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #66      +/-   ##
==========================================
+ Coverage   83.76%   83.94%   +0.17%     
==========================================
  Files          10       10              
  Lines         271      274       +3     
==========================================
+ Hits          227      230       +3     
  Misses         44       44
```


| [Impacted Files](https://codecov.io/gh/mercadona/rele/pull/66?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [rele/subscription.py](https://codecov.io/gh/mercadona/rele/pull/66/diff?src=pr&el=tree#diff-cmVsZS9zdWJzY3JpcHRpb24ucHk=) | `95.74% <100%> (+0.29%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mercadona/rele/pull/66?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mercadona/rele/pull/66?src=pr&el=footer). Last update [72b37a3...0086a7f](https://codecov.io/gh/mercadona/rele/pull/66?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/Makefile b/Makefile
index 99ec70d..9bb9c26 100644
--- a/Makefile
+++ b/Makefile
@@ -47,6 +47,6 @@ install-docs-requirements:  ## install requirements for documentation
 
 install-dev-requirements: install-requirements install-test-requirements install-docs-requirements
 
-build-html-doc:
+build-html-doc: ## builds the project documentation in HTML format
 	DJANGO_SETTINGS_MODULE=tests.settings make html -C docs
 	open docs/_build/html/index.html
diff --git a/rele/subscription.py b/rele/subscription.py
index d288934..2eb5868 100644
--- a/rele/subscription.py
+++ b/rele/subscription.py
@@ -11,11 +11,12 @@ class Subscription:
     """The Subscription class
 
     """
-    def __init__(self, func, topic, prefix='', suffix=''):
+    def __init__(self, func, topic, prefix='', suffix='', filter_by=None):
         self._func = func
         self.topic = topic
         self._prefix = prefix
         self._suffix = suffix
+        self._filter_by = filter_by or (lambda **_: True)
 
     @property
     def name(self):
@@ -33,7 +34,10 @@ class Subscription:
         if 'published_at' in kwargs:
             kwargs['published_at'] = float(kwargs['published_at'])
 
-        return self._func(data, **kwargs)
+        if self._filter_by(**kwargs):
+            return self._func(data, **kwargs)
+
+        return None
 
     def __str__(self):
         return f'{self.name} - {self._func.__name__}'
@@ -65,7 +69,7 @@ class Callback:
             run_middleware_hook('post_process_message')
 
 
-def sub(topic, prefix=None, suffix=None):
+def sub(topic, prefix=None, suffix=None, filter_by=None):
     """Decorator function that makes declaring a PubSub Subscription simple.
 
     The Subscriber returned will automatically create and name
@@ -96,21 +100,28 @@ def sub(topic, prefix=None, suffix=None):
         def purpose_2(data, **kwargs):
              pass
 
+        @sub(topic='photo-updated',
+             filter_by=lambda **attrs: attrs.get('type') == 'landscape')
+        def sub_process_landscape_photos(data, **kwargs):
+            pass
+
     :param topic: string The topic that is being subscribed to.
     :param prefix: string An optional prefix to the subscription name.
                    Useful to namespace your subscription with your project name
     :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.
     :return: :class:`~rele.subscription.Subscription`
     """
 
     def decorator(func):
-        return Subscription(
-            func=func,
-            topic=topic,
-            prefix=prefix,
-            suffix=suffix
-        )
+        return Subscription(func=func,
+                            topic=topic,
+                            prefix=prefix,
+                            suffix=suffix,
+                            filter_by=filter_by)
 
     return decorator


Test patch:
diff --git a/tests/test_subscription.py b/tests/test_subscription.py
index 60d928f..c8ea9ea 100644
--- a/tests/test_subscription.py
+++ b/tests/test_subscription.py
@@ -30,6 +30,15 @@ def sub_published_time_type(data, **kwargs):
     logger.info(f'{type(kwargs["published_at"])}')
 
 
+def landscape_filter(**kwargs):
+    return kwargs.get('type') == 'landscape'
+
+
+@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")}'
+
+
 class TestSubscription:
 
     def test_subs_return_subscription_objects(self):
@@ -49,6 +58,19 @@ class TestSubscription:
         log2 = caplog.records[0]
         assert log2.message == 'I am a task doing stuff with ID 123 (es)'
 
+    def test_sub_executes_when_message_attributes_match_criteria(self, caplog):
+        data = {'name': 'my_new_photo.jpeg'}
+        response = sub_process_landscape_photos(data, type='landscape')
+
+        assert response == 'Received a photo of type landscape'
+
+    def test_sub_does_not_execute_when_message_attributes_dont_match_criteria(
+            self, caplog):
+        data = {'name': 'my_new_photo.jpeg'}
+        response = sub_process_landscape_photos(data, type='')
+
+        assert response is None
+
 
 class TestCallback: