Repository: mercadona/rele
Pull request #170 (2020-06-18 11:50:28)

GC_CREDENTIALS_PATH setting option
### :tophat: What?

Allow settings with credentials path

### :thinking: Why?

Its easier to specify the path of the service account credentials file than it is to make the user instantiate their own credentials object.



Patch:
diff --git a/README.md b/README.md
index 0be43cc..f0d6a49 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ seamless by providing Publisher, Subscriber and Worker classes with the followin
 * Highly Scalable Worker
 * Intuitive Subscription Management
 * Easily Extensible Middleware
-* Optional Django or Flask Integration
+* Ready to go Django/Flask integration
 * CLI
 * And much more!
 
diff --git a/docs/api/settings.rst b/docs/api/settings.rst
index 0d1b516..b3a0f6a 100644
--- a/docs/api/settings.rst
+++ b/docs/api/settings.rst
@@ -22,6 +22,7 @@ Example::
         'GC_CREDENTIALS': service_account.Credentials.from_service_account_file(
             'rele/settings/dummy-credentials.json'
         ),
+        'GC_CREDENTIALS_PATH': 'rele/settings/dummy-credentials.json',
         'GC_PROJECT_ID': 'dummy-project-id',
         'MIDDLEWARE': [
             'rele.contrib.LoggingMiddleware',
@@ -38,9 +39,23 @@ Example::
 ``GC_CREDENTIALS``
 ------------------
 
-**Required**
+**Optional**
+
+Valid Google Service Account credentials object.
+
+.. note:: This method of authentication will be deprecated in favour of
+    ``GC_CREDENTIALS_PATH`` in the future. Therefore it is recommended to use
+    ``GC_CREDENTIALS_PATH``
+
+``GC_CREDENTIALS_PATH``
+-----------------------
+
+**Optional**
+
+Path to service account json file with access to PubSub
 
-Valid Google Service Account credentials.
+.. note:: ``GC_CREDENTIALS`` and ``GC_CREDENTIALS_PATH`` are mutually exclusive.
+    If both are provided, ``GC_CREDENTIALS_PATH`` will be used.
 
 ``GC_PROJECT_ID``
 ------------------
diff --git a/docs/index.rst b/docs/index.rst
index f419ff5..e6d62c9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -29,10 +29,12 @@ classes.
 
 Out of the box, Relé includes the following features:
 
-    * Simple publishing API
-    * Declarative subscribers
-    * Scalable Worker
-    * Ready to install Django/Flask integration
+    * Powerful Publishing API
+    * Highly Scalable Worker
+    * Intuitive Subscription Management
+    * Easily Extensible Middleware
+    * Ready to go Django/Flask integration
+    * CLI
     * And much more...
 
 What It Looks Like
@@ -63,7 +65,7 @@ or with Django integration
 
 .. code::
 
-    $ pip install rele[django]
+    $ pip install rele[django,flask]
 
 User Guides
 ___________
diff --git a/rele/config.py b/rele/config.py
index 9f7a1be..694a4ff 100644
--- a/rele/config.py
+++ b/rele/config.py
@@ -1,6 +1,8 @@
 import importlib
 import os
 
+from google.oauth2 import service_account
+
 from .client import DEFAULT_ACK_DEADLINE, DEFAULT_ENCODER_PATH, get_google_defaults
 from .middleware import default_middleware, register_middleware
 from .publishing import init_global_publisher
@@ -25,8 +27,9 @@ class Config:
         ):
             credentials, project = get_google_defaults()
 
-        self.credentials = setting.get("GC_CREDENTIALS") or credentials
+        self._credentials = setting.get("GC_CREDENTIALS") or credentials
         self.gc_project_id = setting.get("GC_PROJECT_ID") or project
+        self.gc_credentials_path = setting.get("GC_CREDENTIALS_PATH")
         self.app_name = setting.get("APP_NAME")
         self.sub_prefix = setting.get("SUB_PREFIX")
         self.middleware = setting.get("MIDDLEWARE", default_middleware)
@@ -45,6 +48,18 @@ class Config:
         module = importlib.import_module(module_name)
         return getattr(module, class_name)
 
+    @property
+    def credentials(self):
+        if self.gc_credentials_path:
+            return service_account.Credentials.from_service_account_file(
+                self.gc_credentials_path
+            )
+        elif self._credentials:
+            return self._credentials
+
+        else:
+            return None
+
 
 def setup(setting=None, **kwargs):
     if setting is None:
diff --git a/requirements/base.txt b/requirements/base.txt
index 36ddc4e..9c558e3 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -1,1 +1,1 @@
-google-cloud-pubsub
+.
diff --git a/setup.py b/setup.py
index 1298fe1..ed243e8 100644
--- a/setup.py
+++ b/setup.py
@@ -39,7 +39,7 @@ setup(
     url="https://github.com/mercadona/rele",
     packages=find_packages(exclude=("tests",)),
     include_package_data=True,
-    install_requires=["google-cloud-pubsub"],
+    install_requires=["google-auth", "google-cloud-pubsub"],
     extras_require={"django": ["django", "tabulate"], "flask": ["flask"]},
     license="Apache Software License 2.0",
     zip_safe=False,


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 5ac89e0..db48b7d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch
 
 import pytest
 from google.cloud.pubsub_v1 import PublisherClient
+from google.oauth2 import service_account
 
 from rele import Publisher
 from rele.client import Subscriber
@@ -14,12 +15,14 @@ from rele.middleware import register_middleware
 
 @pytest.fixture
 def project_id():
-    return "test-project-id"
+    return "rele-test"
 
 
 @pytest.fixture
 def credentials():
-    return "my-credentials"
+    return service_account.Credentials.from_service_account_file(
+        "tests/dummy-pub-sub-credentials.json"
+    )
 
 
 @pytest.fixture
diff --git a/tests/dummy-pub-sub-credentials.json b/tests/dummy-pub-sub-credentials.json
index 3646b68..2379c7f 100644
--- a/tests/dummy-pub-sub-credentials.json
+++ b/tests/dummy-pub-sub-credentials.json
@@ -1,6 +1,6 @@
 {
   "type": "service_account",
-  "project_id": "rele",
+  "project_id": "rele-test",
   "private_key_id": "ed80f7f63835d4e67ec7d613e65d4d1265c9adb2",
   "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICWgIBAAKBgFgykBgkvE7KsUNU4/el3mFrlGk8G1tO1trR4BoRWttD/wnppzk7\nO+zSeqYpTOKeKNfnz8oQcjVLothVxzhd4+/Id5U0LLVwpT79I9VuhYExMLU9mKYm\niusaJqS+knmB1RkStiZk1n17aTIsaMahgfdcgDUtJI50NksoJHVGyfvfAgMBAAEC\ngYAlFBzACbGg7lXXmLi+RF1ZV4DtPPfDS0HIfLNaQjGQPOXbpP9IcD6hMVuev34z\nR4qkOjCBIqjg/wtXJ7i5Wb+ZcLGYWmKLUTYH425ApIXnCoyn6v9bbIckq8DgNi+o\nzc9OBuVjR3dEWYvmFfn3uv4y8ZR94eaw7vBy+NRa0W9MAQJBAJjEHPXUgb8Ve4/j\nFGQkPQUHnmUXFf8mKDXpDKKiDBFglemFBGo9z69BrNhzQZrHSMbQUMbwxS/Nrnmj\nA2kFZJECQQCTzFx7DAhluvCudaQGdDAZPuuDZwqK+ZArjEZtQn/lrQshlAxzqOwU\njTXsfPliZmiQ3FkKlSKP6tMBsImZSdFvAkARajHe+FW+IcXPNlTJwbPPEfpFject\nCf2Ff8a3938mr/sG/uns7pTxZqw8lI8DBPrP50l+FE52T503MpUd8MZxAkAYrkfD\nRH8ifdUzTPHXIg/mJ1us1cgs7P/mRcZ8+F3jPMJfGRn7Nno19F7M3xHGHNPZXPKB\nkeXzooMaBSD1OB6BAkBZL/b0jSLa2AwP99v5EgEO7+Y8ezv9Hsyb6hVtbDNPBA6Y\ncWDVXslUKXrPfOgJKd/hqRiVixbzNr4b+qIv4vRO\n-----END RSA PRIVATE KEY-----",
   "client_email": "rele@rele.com",
diff --git a/tests/test_config.py b/tests/test_config.py
index 942563c..ec3d8e0 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -2,7 +2,9 @@ import json
 import os
 from unittest.mock import patch
 
+import google
 import pytest
+from google.oauth2 import service_account
 
 from rele import Subscription, sub
 from rele.config import Config, load_subscriptions_from_paths
@@ -67,9 +69,35 @@ class TestConfig:
         assert config.app_name == "rele"
         assert config.sub_prefix == "rele"
         assert config.gc_project_id == project_id
-        assert config.credentials == credentials
+        assert isinstance(config.credentials, service_account.Credentials)
         assert config.middleware == ["rele.contrib.DjangoDBMiddleware"]
 
+    def test_inits_service_account_creds_when_credential_path_given(self, project_id):
+        settings = {
+            "GC_PROJECT_ID": project_id,
+            "GC_CREDENTIALS_PATH": "tests/dummy-pub-sub-credentials.json",
+        }
+
+        config = Config(settings)
+
+        assert config.gc_project_id == project_id
+        assert isinstance(config.credentials, google.oauth2.service_account.Credentials)
+        assert config.credentials.project_id == "rele-test"
+
+    def test_uses_path_instead_of_gc_credentials_when_both_are_provided(
+        self, credentials
+    ):
+        settings = {
+            "GC_CREDENTIALS_PATH": "tests/dummy-pub-sub-credentials.json",
+            "GC_CREDENTIALS": credentials,
+        }
+
+        config = Config(settings)
+
+        assert isinstance(config.credentials, google.oauth2.service_account.Credentials)
+        assert config.credentials != credentials
+        assert config.credentials.project_id == "rele-test"
+
     @patch.dict(os.environ, {"GOOGLE_APPLICATION_CREDENTIALS": ""})
     def test_sets_defaults(self):
         settings = {}
@@ -99,7 +127,7 @@ class TestConfig:
 
         assert config.app_name is None
         assert config.sub_prefix is None
-        assert config.gc_project_id == "rele"
+        assert config.gc_project_id == "rele-test"
         assert config.credentials is not None
         assert config.middleware == ["rele.contrib.LoggingMiddleware"]
         assert config.encoder == json.JSONEncoder
diff --git a/tests/test_worker.py b/tests/test_worker.py
index c39a93e..284f2a9 100644
--- a/tests/test_worker.py
+++ b/tests/test_worker.py
@@ -130,7 +130,7 @@ class TestWorker:
         worker.setup()
 
         assert worker._subscriber._ack_deadline == 60
-        assert worker._subscriber._gc_project_id == "rele"
+        assert worker._subscriber._gc_project_id == "rele-test"
 
 
 class TestCreateAndRun:
@@ -144,11 +144,11 @@ class TestCreateAndRun:
         with patch("rele.worker.Worker", autospec=True) as p:
             yield p
 
-    def test_waits_forever_when_called_with_config_and_subs(self, config, mock_worker):
+    def test_waits_forever_when_called_with_config_and_subs(
+        self, config, mock_worker, credentials
+    ):
         subscriptions = (sub_stub,)
         create_and_run(subscriptions, config)
 
-        mock_worker.assert_called_with(
-            subscriptions, "test-project-id", "my-credentials", 60, 2
-        )
+        mock_worker.assert_called_with(subscriptions, "rele-test", credentials, 60, 2)
         mock_worker.return_value.run_forever.assert_called_once_with()