Repository: mercadona/rele

Issue:
Publishing without boilerplate
## Current Behaviour

Right now, if you want to call `rele.publish()`, you must call `config.setup()` before. This is so that 
`init_global_publisher` is called and setup properly. Otherwise a `ValueError` is raised.

## Proposal
I propose that a user should be able to call `rele.publish` without the boilerplate of `config.setup`. Instead, I think we can call `init_global_publisher` if publishing and there is no global publisher. The tough part will be getting the settings so that the credentials can be configured properly. 

But I do believe this can be solved, to make the UX more elegant and avoid boilerplate. 

Discussion:

Patch:
diff --git a/rele/config.py b/rele/config.py
index 67f97df..da3ae8e 100644
--- a/rele/config.py
+++ b/rele/config.py
@@ -87,7 +87,7 @@ def subscription_from_attribute(attribute):
 
 def load_subscriptions_from_paths(sub_module_paths, sub_prefix=None, filter_by=None):
 
-    subscriptions = []
+    subscriptions = {}
     for sub_module_path in sub_module_paths:
         sub_module = importlib.import_module(sub_module_path)
         for attr_name in dir(sub_module):
@@ -102,5 +102,13 @@ def load_subscriptions_from_paths(sub_module_paths, sub_prefix=None, filter_by=N
             if filter_by and not subscription.filter_by:
                 subscription.set_filters(filter_by)
 
-            subscriptions.append(subscription)
-    return subscriptions
+            if subscription.name in subscriptions:
+                found_subscription = subscriptions[subscription.name]
+                raise RuntimeError(
+                    f"Duplicate subscription name found: {subscription.name}. Subs "
+                    f"{subscription._func.__module__}.{subscription._func.__name__} and "
+                    f"{found_subscription._func.__module__}.{found_subscription._func.__name__} collide."
+                )
+
+            subscriptions[subscription.name] = subscription
+    return list(subscriptions.values())
diff --git a/rele/publishing.py b/rele/publishing.py
index e2bf1d4..3368548 100644
--- a/rele/publishing.py
+++ b/rele/publishing.py
@@ -1,3 +1,5 @@
+from rele import config, discover
+
 from .client import Publisher
 
 _publisher = None
@@ -41,5 +43,10 @@ def publish(topic, data, **kwargs):
     :return: None
     """
     if not _publisher:
-        raise ValueError("init_global_publisher must be called first.")
+        settings, _ = discover.sub_modules()
+        if not hasattr(settings, "RELE"):
+            raise ValueError("Config setup not called and settings module not found.")
+
+        config.setup(settings.RELE)
+
     _publisher.publish(topic, data, **kwargs)


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 00612f0..7a26be4 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -15,6 +15,11 @@ def sub_stub(data, **kwargs):
     return data["id"]
 
 
+@sub(topic="another-cool-topic", prefix="rele")
+def another_sub_stub(data, **kwargs):
+    return data["id"]
+
+
 class TestLoadSubscriptions:
     @pytest.fixture
     def subscriptions(self):
@@ -25,8 +30,8 @@ class TestLoadSubscriptions:
         )
 
     def test_load_subscriptions_in_a_module(self, subscriptions):
-        assert len(subscriptions) == 1
-        func_sub = subscriptions[0]
+        assert len(subscriptions) == 2
+        func_sub = subscriptions[-1]
         assert isinstance(func_sub, Subscription)
         assert func_sub.name == "rele-test-topic"
         assert func_sub({"id": 4}, lang="en") == 4
@@ -44,6 +49,16 @@ class TestLoadSubscriptions:
         assert klass_sub.name == "test-alternative-cool-topic"
         assert klass_sub({"id": 4}, lang="en") == 4
 
+    def test_raises_error_when_subscription_is_duplicated(self):
+        with pytest.raises(RuntimeError) as excinfo:
+            load_subscriptions_from_paths(["tests.test_config", "tests.more_subs.subs"])
+
+        assert (
+            str(excinfo.value)
+            == "Duplicate subscription name found: rele-another-cool-topic. Subs "
+            "tests.more_subs.subs.another_sub_stub and tests.test_config.another_sub_stub collide."
+        )
+
     def test_returns_sub_value_when_filtered_value_applied(self, subscriptions):
 
         assert subscriptions[-1]({"id": 4}, lang="en") == 4
diff --git a/tests/test_publishing.py b/tests/test_publishing.py
index b8bc32f..4c7d4f8 100644
--- a/tests/test_publishing.py
+++ b/tests/test_publishing.py
@@ -1,12 +1,28 @@
 from unittest.mock import MagicMock, patch
 
 import pytest
+from tests import settings
 
 from rele import Publisher, publishing
 
 
 class TestPublish:
-    def test_raises_when_global_publisher_does_not_exist(self):
+    @patch("rele.publishing.Publisher", autospec=True)
+    def test_instantiates_publisher_and_publishes_when_does_not_exist(
+        self, mock_publisher
+    ):
+        with patch("rele.publishing.discover") as mock_discover:
+            mock_discover.sub_modules.return_value = settings, []
+
+            message = {"foo": "bar"}
+            publishing.publish(topic="order-cancelled", data=message, myattr="hello")
+
+            mock_publisher.return_value.publish.assert_called_with(
+                "order-cancelled", {"foo": "bar"}, myattr="hello"
+            )
+
+    def test_raises_error_when_publisher_does_not_exists_and_settings_not_found(self):
+        publishing._publisher = None
         message = {"foo": "bar"}
 
         with pytest.raises(ValueError):
@@ -18,6 +34,7 @@ class TestInitGlobalPublisher:
     def test_creates_global_publisher_when_published_called(
         self, mock_publisher, config
     ):
+        publishing._publisher = None
         mock_publisher.return_value = MagicMock(spec=Publisher)
         publishing.init_global_publisher(config)
         message = {"foo": "bar"}