Repository: mercadona/rele
Pull request #84 (2019-07-11 06:30:26)

ENCODER settings config
### :tophat: What?

Add the `ENCODER` setting to the config object.

### :thinking: Why?

It allows the global publisher to be initialized with the desired encoder. 

### :link: Related issue

Addresses #8 



Patch:
diff --git a/README.md b/README.md
index 9047aa2..a0d53ce 100644
--- a/README.md
+++ b/README.md
@@ -75,6 +75,7 @@ RELE = {
     ],
     'SUB_PREFIX': 'mysubprefix',
     'APP_NAME': 'myappname',
+    'ENCODER_PATH': 'rest_framework.utils.encoders.JSONEncoder',
 }
 ```
 
diff --git a/docs/api/settings.rst b/docs/api/settings.rst
index e20c99b..e541947 100644
--- a/docs/api/settings.rst
+++ b/docs/api/settings.rst
@@ -27,6 +27,7 @@ Example::
         ],
         'SUB_PREFIX': 'mysubprefix',
         'APP_NAME': 'myappname',
+        'ENCODER': 'rest_framework.utils.encoders.JSONEncoder',
     }
 
 
@@ -86,3 +87,17 @@ The application name.
 
 This should be unique to all the services running in the application ecosystem. It is used by
 the LoggingMiddleware and Prometheus integration.
+
+
+``ENCODER_PATH``
+------------------
+
+**Optional**
+
+Default: `rest_framework.utils.encoders.JSONEncoder <https://github.com/encode/django-rest-framework/blob/master/rest_framework/utils/encoders.py#L17>`_
+
+`Encoder class path <https://docs.python.org/3/library/json.html#json.JSONEncoder>`_ to use for
+serializing your Python data structure to a json object when publishing.
+
+NOTE: The default encoder class is subject to change in an upcoming release. It is
+advised that you use this setting explicitly.
diff --git a/rele/client.py b/rele/client.py
index 612d696..dd5ffb0 100644
--- a/rele/client.py
+++ b/rele/client.py
@@ -6,13 +6,13 @@ from contextlib import suppress
 
 from google.api_core import exceptions
 from google.cloud import pubsub_v1
-from rest_framework.utils import encoders
 
 from rele.middleware import run_middleware_hook
 
 logger = logging.getLogger(__name__)
 
 USE_EMULATOR = True if os.environ.get("PUBSUB_EMULATOR_HOST") else False
+DEFAULT_ENCODER_PATH = "rest_framework.utils.encoders.JSONEncoder"
 
 
 class Subscriber:
@@ -68,9 +68,7 @@ class Publisher:
     :param timeout: integer, default 3.0 seconds.
     """
 
-    def __init__(
-        self, gc_project_id, credentials, encoder=encoders.JSONEncoder, timeout=3.0
-    ):
+    def __init__(self, gc_project_id, credentials, encoder, timeout=3.0):
         self._gc_project_id = gc_project_id
         self._timeout = timeout
         self._encoder = encoder
diff --git a/rele/config.py b/rele/config.py
index 0a9f24e..9e63e94 100644
--- a/rele/config.py
+++ b/rele/config.py
@@ -1,8 +1,9 @@
 import importlib
 
-from rele.subscription import Subscription
+from .client import DEFAULT_ENCODER_PATH
 from .middleware import register_middleware, default_middleware
 from .publishing import init_global_publisher
+from .subscription import Subscription
 
 
 class Config:
@@ -22,6 +23,13 @@ class Config:
         self.app_name = setting.get("APP_NAME")
         self.sub_prefix = setting.get("SUB_PREFIX")
         self.middleware = setting.get("MIDDLEWARE", default_middleware)
+        self._encoder_path = setting.get("ENCODER_PATH", DEFAULT_ENCODER_PATH)
+
+    @property
+    def encoder(self):
+        module_name, class_name = self._encoder_path.rsplit(".", 1)
+        module = importlib.import_module(module_name)
+        return getattr(module, class_name)
 
 
 def setup(setting):
diff --git a/rele/publishing.py b/rele/publishing.py
index f8dc04f..0625dcc 100644
--- a/rele/publishing.py
+++ b/rele/publishing.py
@@ -6,7 +6,11 @@ _publisher = None
 def init_global_publisher(config):
     global _publisher
     if not _publisher:
-        _publisher = Publisher(config.gc_project_id, config.credentials)
+        _publisher = Publisher(
+            gc_project_id=config.gc_project_id,
+            credentials=config.credentials,
+            encoder=config.encoder,
+        )
     return _publisher
 
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 9d7c227..8e18014 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,5 @@
+import decimal
+import json
 import pytest
 import concurrent
 from unittest.mock import MagicMock, patch
@@ -39,7 +41,7 @@ def subscriber(project_id, credentials):
 
 @pytest.fixture
 def publisher(config):
-    publisher = Publisher(config.gc_project_id, config.credentials)
+    publisher = Publisher(config.gc_project_id, config.credentials, config.encoder)
     publisher._client = MagicMock(spec=PublisherClient)
     publisher._client.publish.return_value = concurrent.futures.Future()
 
@@ -61,3 +63,13 @@ def time_mock(published_at):
 @pytest.fixture(autouse=True)
 def default_middleware(config):
     register_middleware(config=config)
+
+
+@pytest.fixture
+def custom_encoder():
+    class DecimalEncoder(json.JSONEncoder):
+        def default(self, obj):
+            if isinstance(obj, decimal.Decimal):
+                return float(obj)
+
+    return DecimalEncoder
diff --git a/tests/test_client.py b/tests/test_client.py
index 14f1241..c24d142 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1,4 +1,4 @@
-import json
+import decimal
 import os
 import concurrent
 from unittest.mock import ANY, patch
@@ -45,15 +45,8 @@ class TestPublisher:
             ANY, b'{"foo": "bar"}', published_at=str(published_at)
         )
 
-    def test_publishes_data_with_custom_encoder(self, publisher):
-        import decimal
-
-        class DecimalEncoder(json.JSONEncoder):
-            def default(self, obj):
-                if isinstance(obj, decimal.Decimal):
-                    return float(obj)
-
-        publisher._encoder = DecimalEncoder
+    def test_publishes_data_with_custom_encoder(self, publisher, custom_encoder):
+        publisher._encoder = custom_encoder
         publisher.publish(topic="order-cancelled", data=decimal.Decimal("1.20"))
 
         publisher._client.publish.assert_called_with(ANY, b"1.2", published_at=ANY)
diff --git a/tests/test_config.py b/tests/test_config.py
index 06f8e6c..623d85a 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1,5 +1,7 @@
 import pytest
-from rele.config import load_subscriptions_from_paths
+from rest_framework.utils.encoders import JSONEncoder
+
+from rele.config import load_subscriptions_from_paths, Config
 from rele import sub
 
 
@@ -27,3 +29,35 @@ class TestLoadSubscriptions:
     def test_filter_by_applied_to_subscription_returns_false(self, subscriptions):
 
         assert subscriptions[0].filter_by({"lang": "es"}) is False
+
+
+class TestConfig:
+    def test_parses_all_keys(self, project_id, credentials, custom_encoder):
+        settings = {
+            "APP_NAME": "rele",
+            "SUB_PREFIX": "rele",
+            "GC_PROJECT_ID": project_id,
+            "GC_CREDENTIALS": credentials,
+            "MIDDLEWARE": ["rele.contrib.DjangoDBMiddleware"],
+            "ENCODER": custom_encoder,
+        }
+
+        config = Config(settings)
+
+        assert config.app_name == "rele"
+        assert config.sub_prefix == "rele"
+        assert config.gc_project_id == project_id
+        assert config.credentials == credentials
+        assert config.middleware == ["rele.contrib.DjangoDBMiddleware"]
+
+    def test_sets_defaults(self):
+        settings = {}
+
+        config = Config(settings)
+
+        assert config.app_name is None
+        assert config.sub_prefix is None
+        assert config.gc_project_id is None
+        assert config.credentials is None
+        assert config.middleware == ["rele.contrib.LoggingMiddleware"]
+        assert config.encoder == JSONEncoder