fix(automatic-exchange-rates): skip bad AllRatesToday rows individually

A non-numeric, NaN or infinite value raised while parsing and threw away
the whole table. Each bad row is now skipped on its own.

Log a warning when a table has rows but none of a usable rate type, so a
bank with unknown types does not fail silently.
This commit is contained in:
Herculino Trotta committed 2026-11-08 02:57:07 -03:00
1 parent 43145be24d
commit 6a335b1a63
2 files changed
+44 -4

No files matched your search

@@ -739,10 +739,17 @@ class AllRatesTodayProvider(ExchangeRateProvider):
"""Reduce the table to one rate per pair, in both directions"""
grouped = {}
for row in rows:
if not isinstance(row, dict) or not row.get("value"):
if not isinstance(row, dict):
continue
try:
value = Decimal(str(row.get("value")))
except ArithmeticError:
continue
# A bad cell skips its own row, not the whole table
if not value.is_finite() or value <= 0:
continue
pair = (row.get("base"), row.get("quote"))
grouped.setdefault(pair, {})[row.get("type")] = Decimal(str(row["value"]))
grouped.setdefault(pair, {})[row.get("type")] = value
rates = {}
for (base, quote), values in grouped.items():
@@ -756,11 +763,22 @@ class AllRatesTodayProvider(ExchangeRateProvider):
),
None,
)
if rate is None or rate <= 0:
if rate is None:
logger.debug(
f"AllRatesToday {self.bank}: no usable rate type for "
f"{base}/{quote}, got {sorted(map(str, values))}"
)
continue
rates[(base, quote)] = rate
rates.setdefault((quote, base), self.invert_rate(rate))
if grouped and not rates:
types = sorted({str(t) for values in grouped.values() for t in values})
logger.warning(
f"AllRatesToday table for {self.bank} has no usable rate types, "
f"got {types}"
)
return rates
@staticmethod
@@ -155,13 +155,35 @@ class AllRatesTodayProviderTests(TestCase):
self.assertEqual(self._get_rates(rows, self.brl, self.usd), [])
def test_logs_a_table_without_usable_types(self):
self.usd.exchange_currency = self.brl
payload = {"rates": [_row("USD", "BRL", 5, "monthly_average")]}
with patch.object(
self.provider.session, "get", return_value=_response(payload)
):
with self.assertLogs(
"apps.currencies.exchange_rates.providers", level="WARNING"
) as logs:
self.provider.get_rates([self.usd], {self.brl})
self.assertIn("monthly_average", logs.output[0])
def test_skips_pair_missing_from_the_table(self):
rows = [_row("USD", "BRL", 5)]
self.assertEqual(self._get_rates(rows, self.eur, self.usd), [])
def test_ignores_zero_and_malformed_rows(self):
rows = ["nope", _row("USD", "BRL", 0), _row("EUR", "BRL", 6)]
rows = [
"nope",
_row("USD", "BRL", 0),
_row("USD", "BRL", "5,4"),
_row("USD", "BRL", None),
_row("USD", "BRL", "Infinity"),
_row("USD", "BRL", "NaN"),
_row("EUR", "BRL", 6),
]
rates = self._get_rates(rows, self.brl, self.eur)