We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
feature: Add Meltano command information to Hub API request
### Feature scope

CLI (options, error messages, logging, etc.)

### Description

When Meltano sends requests to the Hub API, currently sets the `X-Project-ID` and `User-Agent` headers: https://github.com/meltano/meltano/blob/193a3ae4c7adfa29e102a65d31b7e865d9eea349/src/meltano/core/hub/client.py#L113-L125

However, it'd be good to include the CLI command where the request is coming from.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/meltano/core/hub/client.py`
Content:
```
1 """Meltano Hub Client."""
2 
3 from __future__ import annotations
4 
5 from http import HTTPStatus
6 from typing import Any
7 
8 import requests
9 from requests.adapters import HTTPAdapter
10 from structlog.stdlib import get_logger
11 from urllib3 import Retry
12 
13 import meltano
14 from meltano.core.plugin import (
15     BasePlugin,
16     PluginDefinition,
17     PluginRef,
18     PluginType,
19     Variant,
20 )
21 from meltano.core.plugin.error import PluginNotFoundError
22 from meltano.core.plugin.factory import base_plugin_factory
23 from meltano.core.plugin_discovery_service import PluginRepository
24 from meltano.core.project import Project
25 from meltano.core.project_settings_service import ProjectSettingsService
26 
27 from .schema import IndexedPlugin, VariantRef
28 
29 logger = get_logger(__name__)
30 
31 
32 class HubPluginTypeNotFoundError(Exception):
33     """Raised when a Hub plugin type is not found."""
34 
35     def __init__(self, plugin_type: PluginType):
36         """Create a new HubPluginVariantNotFound.
37 
38         Args:
39             plugin_type: The type of the plugin.
40         """
41         self.plugin_type = plugin_type
42 
43     def __str__(self) -> str:
44         """Return a string representation of the error.
45 
46         Returns:
47             The string representation of the error.
48         """
49         return "{type} is not supported in Meltano Hub. Available plugin types: {types}".format(
50             type=self.plugin_type.descriptor.capitalize(),
51             types=PluginType.plurals(),
52         )
53 
54 
55 class HubConnectionError(Exception):
56     """Raised when a Hub connection error occurs."""
57 
58     def __init__(self, reason: str):
59         """Create a new HubConnectionError.
60 
61         Args:
62             reason: The reason for the error.
63         """
64         message = f"Could not connect to Meltano Hub. {reason}"
65         super().__init__(message)
66 
67 
68 class HubPluginVariantNotFoundError(Exception):
69     """Raised when a Hub plugin variant is not found."""
70 
71     def __init__(
72         self,
73         plugin_type: PluginType,
74         plugin: IndexedPlugin,
75         variant_name: str,
76     ):
77         """Create a new HubPluginVariantNotFound.
78 
79         Args:
80             plugin_type: The type of the plugin.
81             plugin: The indexed plugin.
82             variant_name: The name of the variant that was not found.
83         """
84         self.plugin_type = plugin_type
85         self.plugin = plugin
86         self.variant_name = variant_name
87 
88     def __str__(self) -> str:
89         """Return a string representation of the error.
90 
91         Returns:
92             The string representation of the error.
93         """
94         return "{type} '{name}' variant '{variant}' is not known to Meltano. Variants: {variant_labels}".format(
95             type=self.plugin_type.descriptor.capitalize(),
96             name=self.plugin.name,
97             variant=self.variant_name,
98             variant_labels=self.plugin.variant_labels,
99         )
100 
101 
102 class MeltanoHubService(PluginRepository):  # noqa: WPS214
103     """PluginRepository implementation for the Meltano Hub."""
104 
105     def __init__(self, project: Project) -> None:
106         """Initialize the service.
107 
108         Args:
109             project: The Meltano project.
110         """
111         self.project = project
112         self.session = requests.Session()
113         self.session.headers.update(
114             {
115                 "Accept": "application/json",
116                 "User-Agent": f"Meltano/{meltano.__version__}",
117             }
118         )
119 
120         self.settings_service = ProjectSettingsService(self.project)
121 
122         if self.settings_service.get("send_anonymous_usage_stats"):
123             project_id = self.settings_service.get("project_id")
124 
125             self.session.headers["X-Project-ID"] = project_id
126 
127         if self.hub_url_auth:
128             self.session.headers.update({"Authorization": self.hub_url_auth})
129 
130         adapter = HTTPAdapter(
131             max_retries=Retry(
132                 total=3,
133                 backoff_factor=0,
134                 status_forcelist=[
135                     HTTPStatus.TOO_MANY_REQUESTS,
136                     HTTPStatus.INTERNAL_SERVER_ERROR,
137                     HTTPStatus.BAD_GATEWAY,
138                     HTTPStatus.SERVICE_UNAVAILABLE,
139                     HTTPStatus.GATEWAY_TIMEOUT,
140                 ],
141                 raise_on_status=False,
142             ),
143         )
144         self.session.mount("http://", adapter)
145         self.session.mount("https://", adapter)
146 
147     @property
148     def hub_api_url(self) -> str:
149         """Return the URL of the Hub API.
150 
151         Returns:
152             The URL of the Hub API.
153         """
154         hub_api_root = self.settings_service.get("hub_api_root")
155         hub_url = self.settings_service.get("hub_url")
156 
157         return hub_api_root or f"{hub_url}/meltano/api/v1"
158 
159     @property
160     def hub_url_auth(self):
161         """Return the `hub_url_auth` setting.
162 
163         Returns:
164             The `hub_url_auth` setting.
165         """
166         return self.settings_service.get("hub_url_auth")
167 
168     def plugin_type_endpoint(self, plugin_type: PluginType) -> str:
169         """Return the list endpoint for the given plugin type.
170 
171         Args:
172             plugin_type: The plugin type.
173 
174         Returns:
175             The endpoint for the given plugin type.
176         """
177         return f"{self.hub_api_url}/plugins/{plugin_type.value}/index"
178 
179     def plugin_endpoint(
180         self,
181         plugin_type: PluginType,
182         plugin_name: str,
183         variant_name: str | None = None,
184     ) -> str:
185         """Return the resource endpoint for the given plugin.
186 
187         Args:
188             plugin_type: The plugin type.
189             plugin_name: The plugin name.
190             variant_name: The plugin variant name.
191 
192         Returns:
193             The endpoint for the given plugin type.
194         """
195         url = f"{self.hub_api_url}/plugins/{plugin_type.value}/{plugin_name}"
196         if variant_name:
197             url = f"{url}--{variant_name}"
198 
199         return url
200 
201     def find_definition(
202         self,
203         plugin_type: PluginType,
204         plugin_name: str,
205         variant_name: str | None = None,
206     ) -> PluginDefinition:
207         """Find a locked plugin definition.
208 
209         Args:
210             plugin_type: The plugin type.
211             plugin_name: The plugin name.
212             variant_name: The plugin variant name.
213 
214         Returns:
215             The plugin definition.
216 
217         Raises:
218             PluginNotFoundError: If the plugin definition could not be found.
219             HubPluginVariantNotFoundError: If the plugin variant could not be found.
220             HubConnectionError: If the Hub API could not be reached.
221         """
222         plugins = self.get_plugins_of_type(plugin_type)
223 
224         try:
225             plugin = plugins[plugin_name]
226         except KeyError as plugins_key_err:
227             raise PluginNotFoundError(
228                 PluginRef(plugin_type, plugin_name)
229             ) from plugins_key_err
230 
231         if variant_name is None or variant_name in {
232             Variant.DEFAULT_NAME,
233             Variant.ORIGINAL_NAME,
234         }:
235             variant_name = plugin.default_variant
236 
237         try:
238             url = plugin.variants[variant_name].ref
239         except KeyError as variant_key_err:
240             raise HubPluginVariantNotFoundError(
241                 plugin_type, plugin, variant_name
242             ) from variant_key_err
243 
244         response = self.session.get(url)
245 
246         try:
247             response.raise_for_status()
248         except requests.HTTPError as http_err:
249             logger.error(
250                 "Can not retrieve plugin",
251                 status_code=http_err.response.status_code,
252                 error=http_err,
253             )
254             raise HubConnectionError(str(http_err)) from http_err
255 
256         return PluginDefinition(**response.json(), plugin_type=plugin_type)
257 
258     def find_base_plugin(
259         self,
260         plugin_type: PluginType,
261         plugin_name: str,
262         variant: str | None = None,
263     ) -> BasePlugin:
264         """Get the base plugin for a project plugin.
265 
266         Args:
267             plugin_type: The plugin type.
268             plugin_name: The plugin name.
269             variant: The plugin variant.
270 
271         Returns:
272             The base plugin.
273         """
274         plugin = self.find_definition(
275             plugin_type,
276             plugin_name,
277             variant_name=variant,
278         )
279 
280         return base_plugin_factory(plugin, plugin.variants[0])
281 
282     def get_plugins_of_type(  # noqa: WPS210
283         self,
284         plugin_type: PluginType,
285     ) -> dict[str, IndexedPlugin]:
286         """Get all plugins of a given type.
287 
288         Args:
289             plugin_type: The plugin type.
290 
291         Returns:
292             The plugin definitions.
293 
294         Raises:
295             HubPluginTypeNotFoundError: If the plugin type is not supported.
296             HubConnectionError: If the Hub API could not be reached.
297         """
298         if not plugin_type.discoverable:
299             return {}
300 
301         url = self.plugin_type_endpoint(plugin_type)
302         response = self.session.get(url)
303 
304         try:
305             response.raise_for_status()
306         except requests.HTTPError as err:
307             logger.error(
308                 "Can not retrieve plugin type",
309                 status_code=err.response.status_code,
310                 error=err,
311             )
312             if err.response.status_code < HTTPStatus.TOO_MANY_REQUESTS:
313                 raise HubPluginTypeNotFoundError(plugin_type) from err
314             raise HubConnectionError(err.response.reason) from err
315 
316         plugins: dict[str, dict[str, Any]] = response.json()
317         return {
318             name: IndexedPlugin(
319                 name,
320                 logo_url=plugin["logo_url"],
321                 default_variant=plugin["default_variant"],
322                 variants={
323                     variant_name: VariantRef(variant_name, ref=variant["ref"])
324                     for variant_name, variant in plugin["variants"].items()
325                 },
326             )
327             for name, plugin in plugins.items()
328         }
329
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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
@@ -5,6 +5,7 @@
 from http import HTTPStatus
 from typing import Any
 
+import click
 import requests
 from requests.adapters import HTTPAdapter
 from structlog.stdlib import get_logger
@@ -198,6 +199,41 @@
 
         return url
 
+    def _build_request(self, method: str, url: str) -> requests.PreparedRequest:
+        """Build a request to the Hub API.
+
+        Args:
+            method: The HTTP method.
+            url: The URL to request.
+
+        Returns:
+            The prepared request.
+        """
+        request = requests.Request(method, url)
+        click_context = click.get_current_context(silent=True)
+
+        if click_context:
+            request.headers["X-Meltano-Command"] = click_context.command_path
+
+        return self.session.prepare_request(request)
+
+    def _get(self, url: str) -> requests.Response:
+        """Make a GET request to the Hub API.
+
+        Args:
+            url: The URL to request.
+
+        Returns:
+            The response.
+
+        Raises:
+            HubConnectionError: If the Hub API could not be reached.
+        """
+        try:
+            return self.session.send(self._build_request("GET", url))
+        except requests.exceptions.ConnectionError as connection_err:
+            raise HubConnectionError("Could not reach Meltano Hub.") from connection_err
+
     def find_definition(
         self,
         plugin_type: PluginType,
@@ -241,7 +277,7 @@
                 plugin_type, plugin, variant_name
             ) from variant_key_err
 
-        response = self.session.get(url)
+        response = self._get(url)
 
         try:
             response.raise_for_status()
@@ -299,7 +335,7 @@
             return {}
 
         url = self.plugin_type_endpoint(plugin_type)
-        response = self.session.get(url)
+        response = self._get(url)
 
         try:
             response.raise_for_status()

{"golden_diff": "diff --git a/src/meltano/core/hub/client.py b/src/meltano/core/hub/client.py\n--- a/src/meltano/core/hub/client.py\n+++ b/src/meltano/core/hub/client.py\n@@ -5,6 +5,7 @@\n from http import HTTPStatus\n from typing import Any\n \n+import click\n import requests\n from requests.adapters import HTTPAdapter\n from structlog.stdlib import get_logger\n@@ -198,6 +199,41 @@\n \n         return url\n \n+    def _build_request(self, method: str, url: str) -> requests.PreparedRequest:\n+        \"\"\"Build a request to the Hub API.\n+\n+        Args:\n+            method: The HTTP method.\n+            url: The URL to request.\n+\n+        Returns:\n+            The prepared request.\n+        \"\"\"\n+        request = requests.Request(method, url)\n+        click_context = click.get_current_context(silent=True)\n+\n+        if click_context:\n+            request.headers[\"X-Meltano-Command\"] = click_context.command_path\n+\n+        return self.session.prepare_request(request)\n+\n+    def _get(self, url: str) -> requests.Response:\n+        \"\"\"Make a GET request to the Hub API.\n+\n+        Args:\n+            url: The URL to request.\n+\n+        Returns:\n+            The response.\n+\n+        Raises:\n+            HubConnectionError: If the Hub API could not be reached.\n+        \"\"\"\n+        try:\n+            return self.session.send(self._build_request(\"GET\", url))\n+        except requests.exceptions.ConnectionError as connection_err:\n+            raise HubConnectionError(\"Could not reach Meltano Hub.\") from connection_err\n+\n     def find_definition(\n         self,\n         plugin_type: PluginType,\n@@ -241,7 +277,7 @@\n                 plugin_type, plugin, variant_name\n             ) from variant_key_err\n \n-        response = self.session.get(url)\n+        response = self._get(url)\n \n         try:\n             response.raise_for_status()\n@@ -299,7 +335,7 @@\n             return {}\n \n         url = self.plugin_type_endpoint(plugin_type)\n-        response = self.session.get(url)\n+        response = self._get(url)\n \n         try:\n             response.raise_for_status()\n", "issue": "feature: Add Meltano command information to Hub API request\n### Feature scope\r\n\r\nCLI (options, error messages, logging, etc.)\r\n\r\n### Description\r\n\r\nWhen Meltano sends requests to the Hub API, currently sets the `X-Project-ID` and `User-Agent` headers: https://github.com/meltano/meltano/blob/193a3ae4c7adfa29e102a65d31b7e865d9eea349/src/meltano/core/hub/client.py#L113-L125\r\n\r\nHowever, it'd be good to include the CLI command where the request is coming from.\n", "before_files": [{"content": "\"\"\"Meltano Hub Client.\"\"\"\n\nfrom __future__ import annotations\n\nfrom http import HTTPStatus\nfrom typing import Any\n\nimport requests\nfrom requests.adapters import HTTPAdapter\nfrom structlog.stdlib import get_logger\nfrom urllib3 import Retry\n\nimport meltano\nfrom meltano.core.plugin import (\n    BasePlugin,\n    PluginDefinition,\n    PluginRef,\n    PluginType,\n    Variant,\n)\nfrom meltano.core.plugin.error import PluginNotFoundError\nfrom meltano.core.plugin.factory import base_plugin_factory\nfrom meltano.core.plugin_discovery_service import PluginRepository\nfrom meltano.core.project import Project\nfrom meltano.core.project_settings_service import ProjectSettingsService\n\nfrom .schema import IndexedPlugin, VariantRef\n\nlogger = get_logger(__name__)\n\n\nclass HubPluginTypeNotFoundError(Exception):\n    \"\"\"Raised when a Hub plugin type is not found.\"\"\"\n\n    def __init__(self, plugin_type: PluginType):\n        \"\"\"Create a new HubPluginVariantNotFound.\n\n        Args:\n            plugin_type: The type of the plugin.\n        \"\"\"\n        self.plugin_type = plugin_type\n\n    def __str__(self) -> str:\n        \"\"\"Return a string representation of the error.\n\n        Returns:\n            The string representation of the error.\n        \"\"\"\n        return \"{type} is not supported in Meltano Hub. Available plugin types: {types}\".format(\n            type=self.plugin_type.descriptor.capitalize(),\n            types=PluginType.plurals(),\n        )\n\n\nclass HubConnectionError(Exception):\n    \"\"\"Raised when a Hub connection error occurs.\"\"\"\n\n    def __init__(self, reason: str):\n        \"\"\"Create a new HubConnectionError.\n\n        Args:\n            reason: The reason for the error.\n        \"\"\"\n        message = f\"Could not connect to Meltano Hub. {reason}\"\n        super().__init__(message)\n\n\nclass HubPluginVariantNotFoundError(Exception):\n    \"\"\"Raised when a Hub plugin variant is not found.\"\"\"\n\n    def __init__(\n        self,\n        plugin_type: PluginType,\n        plugin: IndexedPlugin,\n        variant_name: str,\n    ):\n        \"\"\"Create a new HubPluginVariantNotFound.\n\n        Args:\n            plugin_type: The type of the plugin.\n            plugin: The indexed plugin.\n            variant_name: The name of the variant that was not found.\n        \"\"\"\n        self.plugin_type = plugin_type\n        self.plugin = plugin\n        self.variant_name = variant_name\n\n    def __str__(self) -> str:\n        \"\"\"Return a string representation of the error.\n\n        Returns:\n            The string representation of the error.\n        \"\"\"\n        return \"{type} '{name}' variant '{variant}' is not known to Meltano. Variants: {variant_labels}\".format(\n            type=self.plugin_type.descriptor.capitalize(),\n            name=self.plugin.name,\n            variant=self.variant_name,\n            variant_labels=self.plugin.variant_labels,\n        )\n\n\nclass MeltanoHubService(PluginRepository):  # noqa: WPS214\n    \"\"\"PluginRepository implementation for the Meltano Hub.\"\"\"\n\n    def __init__(self, project: Project) -> None:\n        \"\"\"Initialize the service.\n\n        Args:\n            project: The Meltano project.\n        \"\"\"\n        self.project = project\n        self.session = requests.Session()\n        self.session.headers.update(\n            {\n                \"Accept\": \"application/json\",\n                \"User-Agent\": f\"Meltano/{meltano.__version__}\",\n            }\n        )\n\n        self.settings_service = ProjectSettingsService(self.project)\n\n        if self.settings_service.get(\"send_anonymous_usage_stats\"):\n            project_id = self.settings_service.get(\"project_id\")\n\n            self.session.headers[\"X-Project-ID\"] = project_id\n\n        if self.hub_url_auth:\n            self.session.headers.update({\"Authorization\": self.hub_url_auth})\n\n        adapter = HTTPAdapter(\n            max_retries=Retry(\n                total=3,\n                backoff_factor=0,\n                status_forcelist=[\n                    HTTPStatus.TOO_MANY_REQUESTS,\n                    HTTPStatus.INTERNAL_SERVER_ERROR,\n                    HTTPStatus.BAD_GATEWAY,\n                    HTTPStatus.SERVICE_UNAVAILABLE,\n                    HTTPStatus.GATEWAY_TIMEOUT,\n                ],\n                raise_on_status=False,\n            ),\n        )\n        self.session.mount(\"http://\", adapter)\n        self.session.mount(\"https://\", adapter)\n\n    @property\n    def hub_api_url(self) -> str:\n        \"\"\"Return the URL of the Hub API.\n\n        Returns:\n            The URL of the Hub API.\n        \"\"\"\n        hub_api_root = self.settings_service.get(\"hub_api_root\")\n        hub_url = self.settings_service.get(\"hub_url\")\n\n        return hub_api_root or f\"{hub_url}/meltano/api/v1\"\n\n    @property\n    def hub_url_auth(self):\n        \"\"\"Return the `hub_url_auth` setting.\n\n        Returns:\n            The `hub_url_auth` setting.\n        \"\"\"\n        return self.settings_service.get(\"hub_url_auth\")\n\n    def plugin_type_endpoint(self, plugin_type: PluginType) -> str:\n        \"\"\"Return the list endpoint for the given plugin type.\n\n        Args:\n            plugin_type: The plugin type.\n\n        Returns:\n            The endpoint for the given plugin type.\n        \"\"\"\n        return f\"{self.hub_api_url}/plugins/{plugin_type.value}/index\"\n\n    def plugin_endpoint(\n        self,\n        plugin_type: PluginType,\n        plugin_name: str,\n        variant_name: str | None = None,\n    ) -> str:\n        \"\"\"Return the resource endpoint for the given plugin.\n\n        Args:\n            plugin_type: The plugin type.\n            plugin_name: The plugin name.\n            variant_name: The plugin variant name.\n\n        Returns:\n            The endpoint for the given plugin type.\n        \"\"\"\n        url = f\"{self.hub_api_url}/plugins/{plugin_type.value}/{plugin_name}\"\n        if variant_name:\n            url = f\"{url}--{variant_name}\"\n\n        return url\n\n    def find_definition(\n        self,\n        plugin_type: PluginType,\n        plugin_name: str,\n        variant_name: str | None = None,\n    ) -> PluginDefinition:\n        \"\"\"Find a locked plugin definition.\n\n        Args:\n            plugin_type: The plugin type.\n            plugin_name: The plugin name.\n            variant_name: The plugin variant name.\n\n        Returns:\n            The plugin definition.\n\n        Raises:\n            PluginNotFoundError: If the plugin definition could not be found.\n            HubPluginVariantNotFoundError: If the plugin variant could not be found.\n            HubConnectionError: If the Hub API could not be reached.\n        \"\"\"\n        plugins = self.get_plugins_of_type(plugin_type)\n\n        try:\n            plugin = plugins[plugin_name]\n        except KeyError as plugins_key_err:\n            raise PluginNotFoundError(\n                PluginRef(plugin_type, plugin_name)\n            ) from plugins_key_err\n\n        if variant_name is None or variant_name in {\n            Variant.DEFAULT_NAME,\n            Variant.ORIGINAL_NAME,\n        }:\n            variant_name = plugin.default_variant\n\n        try:\n            url = plugin.variants[variant_name].ref\n        except KeyError as variant_key_err:\n            raise HubPluginVariantNotFoundError(\n                plugin_type, plugin, variant_name\n            ) from variant_key_err\n\n        response = self.session.get(url)\n\n        try:\n            response.raise_for_status()\n        except requests.HTTPError as http_err:\n            logger.error(\n                \"Can not retrieve plugin\",\n                status_code=http_err.response.status_code,\n                error=http_err,\n            )\n            raise HubConnectionError(str(http_err)) from http_err\n\n        return PluginDefinition(**response.json(), plugin_type=plugin_type)\n\n    def find_base_plugin(\n        self,\n        plugin_type: PluginType,\n        plugin_name: str,\n        variant: str | None = None,\n    ) -> BasePlugin:\n        \"\"\"Get the base plugin for a project plugin.\n\n        Args:\n            plugin_type: The plugin type.\n            plugin_name: The plugin name.\n            variant: The plugin variant.\n\n        Returns:\n            The base plugin.\n        \"\"\"\n        plugin = self.find_definition(\n            plugin_type,\n            plugin_name,\n            variant_name=variant,\n        )\n\n        return base_plugin_factory(plugin, plugin.variants[0])\n\n    def get_plugins_of_type(  # noqa: WPS210\n        self,\n        plugin_type: PluginType,\n    ) -> dict[str, IndexedPlugin]:\n        \"\"\"Get all plugins of a given type.\n\n        Args:\n            plugin_type: The plugin type.\n\n        Returns:\n            The plugin definitions.\n\n        Raises:\n            HubPluginTypeNotFoundError: If the plugin type is not supported.\n            HubConnectionError: If the Hub API could not be reached.\n        \"\"\"\n        if not plugin_type.discoverable:\n            return {}\n\n        url = self.plugin_type_endpoint(plugin_type)\n        response = self.session.get(url)\n\n        try:\n            response.raise_for_status()\n        except requests.HTTPError as err:\n            logger.error(\n                \"Can not retrieve plugin type\",\n                status_code=err.response.status_code,\n                error=err,\n            )\n            if err.response.status_code < HTTPStatus.TOO_MANY_REQUESTS:\n                raise HubPluginTypeNotFoundError(plugin_type) from err\n            raise HubConnectionError(err.response.reason) from err\n\n        plugins: dict[str, dict[str, Any]] = response.json()\n        return {\n            name: IndexedPlugin(\n                name,\n                logo_url=plugin[\"logo_url\"],\n                default_variant=plugin[\"default_variant\"],\n                variants={\n                    variant_name: VariantRef(variant_name, ref=variant[\"ref\"])\n                    for variant_name, variant in plugin[\"variants\"].items()\n                },\n            )\n            for name, plugin in plugins.items()\n        }\n", "path": "src/meltano/core/hub/client.py"}], "after_files": [{"content": "\"\"\"Meltano Hub Client.\"\"\"\n\nfrom __future__ import annotations\n\nfrom http import HTTPStatus\nfrom typing import Any\n\nimport click\nimport requests\nfrom requests.adapters import HTTPAdapter\nfrom structlog.stdlib import get_logger\nfrom urllib3 import Retry\n\nimport meltano\nfrom meltano.core.plugin import (\n    BasePlugin,\n    PluginDefinition,\n    PluginRef,\n    PluginType,\n    Variant,\n)\nfrom meltano.core.plugin.error import PluginNotFoundError\nfrom meltano.core.plugin.factory import base_plugin_factory\nfrom meltano.core.plugin_discovery_service import PluginRepository\nfrom meltano.core.project import Project\nfrom meltano.core.project_settings_service import ProjectSettingsService\n\nfrom .schema import IndexedPlugin, VariantRef\n\nlogger = get_logger(__name__)\n\n\nclass HubPluginTypeNotFoundError(Exception):\n    \"\"\"Raised when a Hub plugin type is not found.\"\"\"\n\n    def __init__(self, plugin_type: PluginType):\n        \"\"\"Create a new HubPluginVariantNotFound.\n\n        Args:\n            plugin_type: The type of the plugin.\n        \"\"\"\n        self.plugin_type = plugin_type\n\n    def __str__(self) -> str:\n        \"\"\"Return a string representation of the error.\n\n        Returns:\n            The string representation of the error.\n        \"\"\"\n        return \"{type} is not supported in Meltano Hub. Available plugin types: {types}\".format(\n            type=self.plugin_type.descriptor.capitalize(),\n            types=PluginType.plurals(),\n        )\n\n\nclass HubConnectionError(Exception):\n    \"\"\"Raised when a Hub connection error occurs.\"\"\"\n\n    def __init__(self, reason: str):\n        \"\"\"Create a new HubConnectionError.\n\n        Args:\n            reason: The reason for the error.\n        \"\"\"\n        message = f\"Could not connect to Meltano Hub. {reason}\"\n        super().__init__(message)\n\n\nclass HubPluginVariantNotFoundError(Exception):\n    \"\"\"Raised when a Hub plugin variant is not found.\"\"\"\n\n    def __init__(\n        self,\n        plugin_type: PluginType,\n        plugin: IndexedPlugin,\n        variant_name: str,\n    ):\n        \"\"\"Create a new HubPluginVariantNotFound.\n\n        Args:\n            plugin_type: The type of the plugin.\n            plugin: The indexed plugin.\n            variant_name: The name of the variant that was not found.\n        \"\"\"\n        self.plugin_type = plugin_type\n        self.plugin = plugin\n        self.variant_name = variant_name\n\n    def __str__(self) -> str:\n        \"\"\"Return a string representation of the error.\n\n        Returns:\n            The string representation of the error.\n        \"\"\"\n        return \"{type} '{name}' variant '{variant}' is not known to Meltano. Variants: {variant_labels}\".format(\n            type=self.plugin_type.descriptor.capitalize(),\n            name=self.plugin.name,\n            variant=self.variant_name,\n            variant_labels=self.plugin.variant_labels,\n        )\n\n\nclass MeltanoHubService(PluginRepository):  # noqa: WPS214\n    \"\"\"PluginRepository implementation for the Meltano Hub.\"\"\"\n\n    def __init__(self, project: Project) -> None:\n        \"\"\"Initialize the service.\n\n        Args:\n            project: The Meltano project.\n        \"\"\"\n        self.project = project\n        self.session = requests.Session()\n        self.session.headers.update(\n            {\n                \"Accept\": \"application/json\",\n                \"User-Agent\": f\"Meltano/{meltano.__version__}\",\n            }\n        )\n\n        self.settings_service = ProjectSettingsService(self.project)\n\n        if self.settings_service.get(\"send_anonymous_usage_stats\"):\n            project_id = self.settings_service.get(\"project_id\")\n\n            self.session.headers[\"X-Project-ID\"] = project_id\n\n        if self.hub_url_auth:\n            self.session.headers.update({\"Authorization\": self.hub_url_auth})\n\n        adapter = HTTPAdapter(\n            max_retries=Retry(\n                total=3,\n                backoff_factor=0,\n                status_forcelist=[\n                    HTTPStatus.TOO_MANY_REQUESTS,\n                    HTTPStatus.INTERNAL_SERVER_ERROR,\n                    HTTPStatus.BAD_GATEWAY,\n                    HTTPStatus.SERVICE_UNAVAILABLE,\n                    HTTPStatus.GATEWAY_TIMEOUT,\n                ],\n                raise_on_status=False,\n            ),\n        )\n        self.session.mount(\"http://\", adapter)\n        self.session.mount(\"https://\", adapter)\n\n    @property\n    def hub_api_url(self) -> str:\n        \"\"\"Return the URL of the Hub API.\n\n        Returns:\n            The URL of the Hub API.\n        \"\"\"\n        hub_api_root = self.settings_service.get(\"hub_api_root\")\n        hub_url = self.settings_service.get(\"hub_url\")\n\n        return hub_api_root or f\"{hub_url}/meltano/api/v1\"\n\n    @property\n    def hub_url_auth(self):\n        \"\"\"Return the `hub_url_auth` setting.\n\n        Returns:\n            The `hub_url_auth` setting.\n        \"\"\"\n        return self.settings_service.get(\"hub_url_auth\")\n\n    def plugin_type_endpoint(self, plugin_type: PluginType) -> str:\n        \"\"\"Return the list endpoint for the given plugin type.\n\n        Args:\n            plugin_type: The plugin type.\n\n        Returns:\n            The endpoint for the given plugin type.\n        \"\"\"\n        return f\"{self.hub_api_url}/plugins/{plugin_type.value}/index\"\n\n    def plugin_endpoint(\n        self,\n        plugin_type: PluginType,\n        plugin_name: str,\n        variant_name: str | None = None,\n    ) -> str:\n        \"\"\"Return the resource endpoint for the given plugin.\n\n        Args:\n            plugin_type: The plugin type.\n            plugin_name: The plugin name.\n            variant_name: The plugin variant name.\n\n        Returns:\n            The endpoint for the given plugin type.\n        \"\"\"\n        url = f\"{self.hub_api_url}/plugins/{plugin_type.value}/{plugin_name}\"\n        if variant_name:\n            url = f\"{url}--{variant_name}\"\n\n        return url\n\n    def _build_request(self, method: str, url: str) -> requests.PreparedRequest:\n        \"\"\"Build a request to the Hub API.\n\n        Args:\n            method: The HTTP method.\n            url: The URL to request.\n\n        Returns:\n            The prepared request.\n        \"\"\"\n        request = requests.Request(method, url)\n        click_context = click.get_current_context(silent=True)\n\n        if click_context:\n            request.headers[\"X-Meltano-Command\"] = click_context.command_path\n\n        return self.session.prepare_request(request)\n\n    def _get(self, url: str) -> requests.Response:\n        \"\"\"Make a GET request to the Hub API.\n\n        Args:\n            url: The URL to request.\n\n        Returns:\n            The response.\n\n        Raises:\n            HubConnectionError: If the Hub API could not be reached.\n        \"\"\"\n        try:\n            return self.session.send(self._build_request(\"GET\", url))\n        except requests.exceptions.ConnectionError as connection_err:\n            raise HubConnectionError(\"Could not reach Meltano Hub.\") from connection_err\n\n    def find_definition(\n        self,\n        plugin_type: PluginType,\n        plugin_name: str,\n        variant_name: str | None = None,\n    ) -> PluginDefinition:\n        \"\"\"Find a locked plugin definition.\n\n        Args:\n            plugin_type: The plugin type.\n            plugin_name: The plugin name.\n            variant_name: The plugin variant name.\n\n        Returns:\n            The plugin definition.\n\n        Raises:\n            PluginNotFoundError: If the plugin definition could not be found.\n            HubPluginVariantNotFoundError: If the plugin variant could not be found.\n            HubConnectionError: If the Hub API could not be reached.\n        \"\"\"\n        plugins = self.get_plugins_of_type(plugin_type)\n\n        try:\n            plugin = plugins[plugin_name]\n        except KeyError as plugins_key_err:\n            raise PluginNotFoundError(\n                PluginRef(plugin_type, plugin_name)\n            ) from plugins_key_err\n\n        if variant_name is None or variant_name in {\n            Variant.DEFAULT_NAME,\n            Variant.ORIGINAL_NAME,\n        }:\n            variant_name = plugin.default_variant\n\n        try:\n            url = plugin.variants[variant_name].ref\n        except KeyError as variant_key_err:\n            raise HubPluginVariantNotFoundError(\n                plugin_type, plugin, variant_name\n            ) from variant_key_err\n\n        response = self._get(url)\n\n        try:\n            response.raise_for_status()\n        except requests.HTTPError as http_err:\n            logger.error(\n                \"Can not retrieve plugin\",\n                status_code=http_err.response.status_code,\n                error=http_err,\n            )\n            raise HubConnectionError(str(http_err)) from http_err\n\n        return PluginDefinition(**response.json(), plugin_type=plugin_type)\n\n    def find_base_plugin(\n        self,\n        plugin_type: PluginType,\n        plugin_name: str,\n        variant: str | None = None,\n    ) -> BasePlugin:\n        \"\"\"Get the base plugin for a project plugin.\n\n        Args:\n            plugin_type: The plugin type.\n            plugin_name: The plugin name.\n            variant: The plugin variant.\n\n        Returns:\n            The base plugin.\n        \"\"\"\n        plugin = self.find_definition(\n            plugin_type,\n            plugin_name,\n            variant_name=variant,\n        )\n\n        return base_plugin_factory(plugin, plugin.variants[0])\n\n    def get_plugins_of_type(  # noqa: WPS210\n        self,\n        plugin_type: PluginType,\n    ) -> dict[str, IndexedPlugin]:\n        \"\"\"Get all plugins of a given type.\n\n        Args:\n            plugin_type: The plugin type.\n\n        Returns:\n            The plugin definitions.\n\n        Raises:\n            HubPluginTypeNotFoundError: If the plugin type is not supported.\n            HubConnectionError: If the Hub API could not be reached.\n        \"\"\"\n        if not plugin_type.discoverable:\n            return {}\n\n        url = self.plugin_type_endpoint(plugin_type)\n        response = self._get(url)\n\n        try:\n            response.raise_for_status()\n        except requests.HTTPError as err:\n            logger.error(\n                \"Can not retrieve plugin type\",\n                status_code=err.response.status_code,\n                error=err,\n            )\n            if err.response.status_code < HTTPStatus.TOO_MANY_REQUESTS:\n                raise HubPluginTypeNotFoundError(plugin_type) from err\n            raise HubConnectionError(err.response.reason) from err\n\n        plugins: dict[str, dict[str, Any]] = response.json()\n        return {\n            name: IndexedPlugin(\n                name,\n                logo_url=plugin[\"logo_url\"],\n                default_variant=plugin[\"default_variant\"],\n                variants={\n                    variant_name: VariantRef(variant_name, ref=variant[\"ref\"])\n                    for variant_name, variant in plugin[\"variants\"].items()\n                },\n            )\n            for name, plugin in plugins.items()\n        }\n", "path": "src/meltano/core/hub/client.py"}]}