Repository: meltano/meltano

Issue:
Make the Hub base URL configurable in Meltano
We should probably make this configurable, at least for testing purposes.

It would be nice to be able to stand up the Hub API from its main branch (or a feature one) and test Meltano against it directly instead of mocking it entirely.

_Originally posted by @edgarrmondragon in https://github.com/meltano/meltano/pull/5925#discussion_r885158727_


Discussion:

Resolving patch:
diff --git a/src/meltano/core/hub/client.py b/src/meltano/core/hub/client.py
--- a/src/meltano/core/hub/client.py
+++ b/src/meltano/core/hub/client.py
@@ -19,6 +19,7 @@
 from meltano.core.plugin.factory import base_plugin_factory
 from meltano.core.plugin_discovery_service import PluginRepository
 from meltano.core.project import Project
+from meltano.core.project_settings_service import ProjectSettingsService
 
 from .schema import IndexedPlugin, VariantRef
 
@@ -85,8 +86,6 @@ def __str__(self) -> str:
 class MeltanoHubService(PluginRepository):
     """PluginRepository implementation for the Meltano Hub."""
 
-    BASE_URL = "https://hub.meltano.com/meltano/api/v1"
-
     def __init__(self, project: Project) -> None:
         """Initialize the service.
 
@@ -102,6 +101,23 @@ def __init__(self, project: Project) -> None:
             }
         )
 
+        self.settings_service = ProjectSettingsService(self.project)
+
+        if self.settings_service.get("send_anonymous_usage_stats"):
+            project_id = self.settings_service.get("project_id")
+
+            self.session.headers["X-Project-ID"] = project_id
+
+    @property
+    def hub_api_url(self):
+        """Return the URL of the Hub API.
+
+        Returns:
+            The URL of the Hub API.
+        """
+        hub_url = self.settings_service.get("hub_url")
+        return f"{hub_url}/meltano/api/v1"
+
     def plugin_type_endpoint(self, plugin_type: PluginType) -> str:
         """Return the list endpoint for the given plugin type.
 
@@ -111,7 +127,7 @@ def plugin_type_endpoint(self, plugin_type: PluginType) -> str:
         Returns:
             The endpoint for the given plugin type.
         """
-        return f"{self.BASE_URL}/plugins/{plugin_type.value}/index"
+        return f"{self.hub_api_url}/plugins/{plugin_type.value}/index"
 
     def plugin_endpoint(
         self,
@@ -129,7 +145,7 @@ def plugin_endpoint(
         Returns:
             The endpoint for the given plugin type.
         """
-        url = f"{self.BASE_URL}/plugins/{plugin_type.value}/{plugin_name}"
+        url = f"{self.hub_api_url}/plugins/{plugin_type.value}/{plugin_name}"
         if variant_name:
             url = f"{url}--{variant_name}"
 
diff --git a/src/meltano/core/settings_store.py b/src/meltano/core/settings_store.py
--- a/src/meltano/core/settings_store.py
+++ b/src/meltano/core/settings_store.py
@@ -38,6 +38,7 @@ def __init__(self, *setting_names):
             setting_names: the name/aliases where conflicting values are set
 
         """
+        self.setting_names = setting_names
         super().__init__()
 
     def __str__(self) -> str:


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -69,7 +69,7 @@ def setup_env():
 
 
 class MockAdapter(BaseAdapter):
-    def _process_discovery(self, base_url: str, discovery: dict) -> dict:
+    def _process_discovery(self, api_url: str, discovery: dict) -> dict:
         hub = {}
         for plugin_type in PluginType:
             index_key = f"/{plugin_type}/index"
@@ -89,7 +89,7 @@ def _process_discovery(self, base_url: str, discovery: dict) -> dict:
                         default_variant = variant_name
 
                     hub[index_key][plugin_name]["variants"][variant_name] = {
-                        "ref": f"{base_url}/plugins/{plugin_type}/{plugin_name}--{variant_name}"
+                        "ref": f"{api_url}/plugins/{plugin_type}/{plugin_name}--{variant_name}"
                     }
 
                     plugin_key = f"/{plugin_type}/{plugin_name}--{variant_name}"
@@ -107,7 +107,7 @@ def _process_discovery(self, base_url: str, discovery: dict) -> dict:
                     default_variant = variant_name
 
                     hub[index_key][plugin_name]["variants"][variant_name] = {
-                        "ref": f"{base_url}/plugins/{plugin_type}/{plugin_name}--{variant_name}"
+                        "ref": f"{api_url}/plugins/{plugin_type}/{plugin_name}--{variant_name}"
                     }
 
                     plugin_key = f"/{plugin_type}/{plugin_name}--{variant_name}"
@@ -133,17 +133,17 @@ def _process_discovery(self, base_url: str, discovery: dict) -> dict:
 
         return hub
 
-    def __init__(self, base_url: str, discovery: dict) -> None:
+    def __init__(self, api_url: str, discovery: dict) -> None:
         """Create a mock HTTP adapter for the Hub.
 
         Args:
-            base_url: The base URL of the Hub.
+            api_url: The base URL of the Hub.
             discovery: A parsed discovery.yml file.
         """
         super().__init__()
-        self.base_url = base_url
+        self.api_url = api_url
         self.count = Counter()
-        self._mapping = self._process_discovery(base_url, deepcopy(discovery))
+        self._mapping = self._process_discovery(api_url, deepcopy(discovery))
 
     def send(
         self,
@@ -156,6 +156,7 @@ def send(
     ):
         _, endpoint = request.path_url.split("/meltano/api/v1/plugins")
         response = requests.Response()
+        response.request = request
 
         try:
             data = self._mapping[endpoint]
@@ -173,14 +174,14 @@ def send(
 @pytest.fixture(scope="class")
 def meltano_hub_service(project, discovery):
     hub = MeltanoHubService(project)
-    hub.session.mount(hub.BASE_URL, MockAdapter(hub.BASE_URL, discovery))
+    hub.session.mount(hub.hub_api_url, MockAdapter(hub.hub_api_url, discovery))
     return hub
 
 
 @pytest.fixture(scope="function")
 def hub_request_counter(meltano_hub_service: MeltanoHubService):
     counter: Counter = meltano_hub_service.session.get_adapter(
-        meltano_hub_service.BASE_URL
+        meltano_hub_service.hub_api_url
     ).count
     counter.clear()
     return counter
diff --git a/tests/meltano/cli/test_discover.py b/tests/meltano/cli/test_discover.py
--- a/tests/meltano/cli/test_discover.py
+++ b/tests/meltano/cli/test_discover.py
@@ -15,7 +15,9 @@ def test_discover(
         meltano_hub_service: MeltanoHubService,
         hub_request_counter: Counter,
     ):
-        adapter = meltano_hub_service.session.get_adapter(meltano_hub_service.BASE_URL)
+        adapter = meltano_hub_service.session.get_adapter(
+            meltano_hub_service.hub_api_url
+        )
 
         with mock.patch("requests.adapters.HTTPAdapter.send", adapter.send):
             result = cli_runner.invoke(cli, ["discover"])
@@ -42,7 +44,9 @@ def test_discover_extractors(
         meltano_hub_service: MeltanoHubService,
         hub_request_counter: Counter,
     ):
-        adapter = meltano_hub_service.session.get_adapter(meltano_hub_service.BASE_URL)
+        adapter = meltano_hub_service.session.get_adapter(
+            meltano_hub_service.hub_api_url,
+        )
 
         with mock.patch("requests.adapters.HTTPAdapter.send", adapter.send):
             result = cli_runner.invoke(cli, ["discover", "extractors"])