Repository: rotki/rotki

Issue:
Disabling sync for a exchange disables all the exchanges in the same location
## Problem Definition

Currently we allow do disable sync in a per exchange instance way, that is `location + name` but in the code we are doing

```python
with self.db.conn.read_ctx() as cursor:
    excluded_locations = {exchange.location for exchange in self.db.get_settings(cursor).non_syncing_exchanges}  # noqa: E501

if location is not None:
    if location not in excluded_locations:
        self.query_location_latest_trades(location=location, from_ts=from_ts, to_ts=to_ts)

    return
```

that ignores all the exchanges in the same location and not just the combination `location + name`

![imagen](https://github.com/rotki/rotki/assets/5068010/7f7ecefa-02fb-4900-a202-b42b8ce8451a)

### System Description


Operating system: macOS
Rotki version: 1.31.1



Discussion:

Resolving patch:
diff --git a/rotkehlchen/history/events.py b/rotkehlchen/history/events.py
--- a/rotkehlchen/history/events.py
+++ b/rotkehlchen/history/events.py
@@ -1,4 +1,5 @@
 import logging
+from collections import defaultdict
 from pathlib import Path
 from typing import TYPE_CHECKING, Literal
 
@@ -91,13 +92,19 @@ def _query_services_for_trades(self, filter_query: TradesFilterQuery) -> None:
         location = filter_query.location
         from_ts = filter_query.from_ts
         to_ts = filter_query.to_ts
+        excluded_instances: dict[Location, set[str]] = defaultdict(set[str])
 
         with self.db.conn.read_ctx() as cursor:
-            excluded_locations = {exchange.location for exchange in self.db.get_settings(cursor).non_syncing_exchanges}  # noqa: E501
+            for exchange_id in self.db.get_settings(cursor).non_syncing_exchanges:
+                excluded_instances[exchange_id.location].add(exchange_id.name)
 
         if location is not None:
-            if location not in excluded_locations:
-                self.query_location_latest_trades(location=location, from_ts=from_ts, to_ts=to_ts)
+            self.query_location_latest_trades(
+                location=location,
+                excluded_instances=excluded_instances[location],
+                from_ts=from_ts,
+                to_ts=to_ts,
+            )
 
             return
 
@@ -144,10 +151,12 @@ def query_trades(
     def query_location_latest_trades(
             self,
             location: Location,
+            excluded_instances: set[str],
             from_ts: Timestamp,
             to_ts: Timestamp,
     ) -> None:
         """Queries the service of a specific location for latest trades and saves them in the DB.
+        Services whose names are present in the excluded_instances set are not queried.
         May raise:
 
         - RemoteError if there is a problem with reaching the service
@@ -160,11 +169,12 @@ def query_location_latest_trades(
             return
 
         for exchange in exchanges_list:
-            exchange.query_trade_history(
-                start_ts=from_ts,
-                end_ts=to_ts,
-                only_cache=False,
-            )
+            if exchange.name not in excluded_instances:
+                exchange.query_trade_history(
+                    start_ts=from_ts,
+                    end_ts=to_ts,
+                    only_cache=False,
+                )
 
     def _query_services_for_asset_movements(self, filter_query: AssetMovementsFilterQuery) -> None:
         """Queries all services requested for asset movements and writes them to the DB"""


Test patch:
diff --git a/rotkehlchen/tests/unit/test_trades.py b/rotkehlchen/tests/unit/test_trades.py
--- a/rotkehlchen/tests/unit/test_trades.py
+++ b/rotkehlchen/tests/unit/test_trades.py
@@ -8,6 +8,7 @@
 from rotkehlchen.errors.asset import UnknownAsset
 from rotkehlchen.exchanges.data_structures import Trade, deserialize_trade, trades_from_dictlist
 from rotkehlchen.fval import FVal
+from rotkehlchen.tests.utils.exchanges import create_test_coinbase
 from rotkehlchen.types import ExchangeLocationID, Location, Timestamp, TradeType
 from rotkehlchen.utils.serialization import rlk_jsondumps
 
@@ -132,19 +133,47 @@ def test_serialize_deserialize_trade():
 
 @pytest.mark.parametrize('db_settings', [
     {'non_syncing_exchanges': [ExchangeLocationID(name='Binance', location=Location.BINANCE)]}])
-def test_query_trade_history_online_but_exchange_excluded(events_historian):
-    """Test that if an online refresh of trades for an exchange is requested,
-    that exchange is not connected, but is also at the excluded exchanges we
-    don't end up querying trades of all exchanges"""
-
-    patch_latest_trades = patch.object(events_historian, 'query_location_latest_trades')
-    patch_iterate_exchanges = patch.object(events_historian.exchange_manager, 'iterate_exchanges')
-
-    with patch_latest_trades as latest_trades_mock, patch_iterate_exchanges as iterate_exchanges_mock:  # noqa: E501
+def test_query_trade_history_online_but_exchange_excluded(events_historian, function_scope_binance):  # noqa: E501
+    """
+    Test that if an online refresh of trades for an exchange is requested but is also at
+    the excluded exchanges we don't end up querying trades of all exchanges.
+    """
+    with (
+        patch.object(target=events_historian.exchange_manager, attribute='iterate_exchanges') as iterate_exchanges_mock,  # noqa: E501
+        patch.object(target=function_scope_binance, attribute='query_trade_history') as patch_query_trade_history,  # noqa: E501
+    ):
         events_historian.query_trades(
             filter_query=TradesFilterQuery.make(location=Location.BINANCE),
             only_cache=False,
         )
 
-    assert latest_trades_mock.call_count == 0
+    assert patch_query_trade_history.call_count == 0
     assert iterate_exchanges_mock.call_count == 0
+
+
+@pytest.mark.parametrize('db_settings', [
+    {'non_syncing_exchanges': [ExchangeLocationID(name='Coinbase', location=Location.COINBASE)]}])
+def test_query_trade_history_with_exchange_instance_excluded(events_historian):
+    """
+    Test that when an exchange is ignored and more instances of the same location exist
+    we only ignore the correct instance and not all.
+    """
+
+    coinbase_instances = [create_test_coinbase(
+        name=name,
+        database=events_historian,
+        msg_aggregator=events_historian.msg_aggregator,
+    ) for name in ('Coinbase', 'Coinbase2')]
+
+    events_historian.exchange_manager.connected_exchanges[Location.COINBASE] = coinbase_instances
+    with (
+        patch.object(target=coinbase_instances[0], attribute='query_trade_history') as coinbase_mock,  # noqa: E501
+        patch.object(target=coinbase_instances[1], attribute='query_trade_history') as coinbase2_mock,  # noqa: E501
+    ):
+        events_historian.query_trades(
+            filter_query=TradesFilterQuery.make(location=Location.COINBASE),
+            only_cache=False,
+        )
+
+    assert coinbase_mock.call_count == 0
+    assert coinbase2_mock.call_count == 1
diff --git a/rotkehlchen/tests/utils/exchanges.py b/rotkehlchen/tests/utils/exchanges.py
--- a/rotkehlchen/tests/utils/exchanges.py
+++ b/rotkehlchen/tests/utils/exchanges.py
@@ -557,9 +557,10 @@ def mock_poloniex_asset_return(url, *args, **kwargs):  # pylint: disable=unused-
 def create_test_coinbase(
         database: DBHandler,
         msg_aggregator: MessagesAggregator,
+        name: str = 'coinbase',
 ) -> Coinbase:
     mock = Coinbase(
-        name='coinbase',
+        name=name,
         api_key=make_api_key(),
         secret=make_api_secret(),
         database=database,