mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-04-23 00:58:40 +02:00
feat(currencies): add Frankfurter as an Exchange Rate provider
This commit is contained in:
@@ -4,13 +4,7 @@ from datetime import timedelta
|
|||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from apps.currencies.exchange_rates.providers import (
|
import apps.currencies.exchange_rates.providers as providers
|
||||||
SynthFinanceProvider,
|
|
||||||
SynthFinanceStockProvider,
|
|
||||||
CoinGeckoFreeProvider,
|
|
||||||
CoinGeckoProProvider,
|
|
||||||
TransitiveRateProvider,
|
|
||||||
)
|
|
||||||
from apps.currencies.models import ExchangeRateService, ExchangeRate, Currency
|
from apps.currencies.models import ExchangeRateService, ExchangeRate, Currency
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -18,11 +12,12 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Map service types to provider classes
|
# Map service types to provider classes
|
||||||
PROVIDER_MAPPING = {
|
PROVIDER_MAPPING = {
|
||||||
"synth_finance": SynthFinanceProvider,
|
"synth_finance": providers.SynthFinanceProvider,
|
||||||
"synth_finance_stock": SynthFinanceStockProvider,
|
"synth_finance_stock": providers.SynthFinanceStockProvider,
|
||||||
"coingecko_free": CoinGeckoFreeProvider,
|
"coingecko_free": providers.CoinGeckoFreeProvider,
|
||||||
"coingecko_pro": CoinGeckoProProvider,
|
"coingecko_pro": providers.CoinGeckoProProvider,
|
||||||
"transitive": TransitiveRateProvider,
|
"transitive": providers.TransitiveRateProvider,
|
||||||
|
"frankfurter": providers.FrankfurterProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -306,3 +306,87 @@ class TransitiveRateProvider(ExchangeRateProvider):
|
|||||||
queue.append((neighbor, path + [neighbor], current_rate * rate))
|
queue.append((neighbor, path + [neighbor], current_rate * rate))
|
||||||
|
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
class FrankfurterProvider(ExchangeRateProvider):
|
||||||
|
"""Implementation for the Frankfurter API (frankfurter.dev)"""
|
||||||
|
|
||||||
|
BASE_URL = "https://api.frankfurter.dev/v1/latest"
|
||||||
|
rates_inverted = (
|
||||||
|
False # Frankfurter returns non-inverted rates (e.g., 1 EUR = 1.1 USD)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, api_key: str = None):
|
||||||
|
"""
|
||||||
|
Initializes the provider. The Frankfurter API does not require an API key,
|
||||||
|
so the api_key parameter is ignored.
|
||||||
|
"""
|
||||||
|
super().__init__(api_key)
|
||||||
|
self.session = requests.Session()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def requires_api_key(cls) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_rates(
|
||||||
|
self, target_currencies: QuerySet, exchange_currencies: set
|
||||||
|
) -> List[Tuple[Currency, Currency, Decimal]]:
|
||||||
|
results = []
|
||||||
|
currency_groups = {}
|
||||||
|
# Group target currencies by their exchange (base) currency to minimize API calls
|
||||||
|
for currency in target_currencies:
|
||||||
|
if currency.exchange_currency in exchange_currencies:
|
||||||
|
group = currency_groups.setdefault(currency.exchange_currency.code, [])
|
||||||
|
group.append(currency)
|
||||||
|
|
||||||
|
# Make one API call for each base currency
|
||||||
|
for base_currency, currencies in currency_groups.items():
|
||||||
|
try:
|
||||||
|
# Create a comma-separated list of target currency codes
|
||||||
|
to_currencies = ",".join(
|
||||||
|
currency.code
|
||||||
|
for currency in currencies
|
||||||
|
if currency.code != base_currency
|
||||||
|
)
|
||||||
|
|
||||||
|
# If there are no target currencies other than the base, skip the API call
|
||||||
|
if not to_currencies:
|
||||||
|
# Handle the case where the only request is for the base rate (e.g., USD to USD)
|
||||||
|
for currency in currencies:
|
||||||
|
if currency.code == base_currency:
|
||||||
|
results.append(
|
||||||
|
(currency.exchange_currency, currency, Decimal("1"))
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
response = self.session.get(
|
||||||
|
self.BASE_URL,
|
||||||
|
params={"base": base_currency, "symbols": to_currencies},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
rates = data["rates"]
|
||||||
|
|
||||||
|
# Process the returned rates
|
||||||
|
for currency in currencies:
|
||||||
|
if currency.code == base_currency:
|
||||||
|
# The rate for the base currency to itself is always 1
|
||||||
|
rate = Decimal("1")
|
||||||
|
else:
|
||||||
|
rate = Decimal(str(rates[currency.code]))
|
||||||
|
|
||||||
|
results.append((currency.exchange_currency, currency, rate))
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error fetching rates from Frankfurter API for base {base_currency}: {e}"
|
||||||
|
)
|
||||||
|
except KeyError as e:
|
||||||
|
logger.error(
|
||||||
|
f"Unexpected response structure from Frankfurter API for base {base_currency}: {e}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Unexpected error processing Frankfurter data for base {base_currency}: {e}"
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.5 on 2025-08-16 22:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('currencies', '0016_alter_exchangerate_automatic'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='exchangerateservice',
|
||||||
|
name='service_type',
|
||||||
|
field=models.CharField(choices=[('synth_finance', 'Synth Finance'), ('synth_finance_stock', 'Synth Finance Stock'), ('coingecko_free', 'CoinGecko (Demo/Free)'), ('coingecko_pro', 'CoinGecko (Pro)'), ('transitive', 'Transitive (Calculated from Existing Rates)'), ('frankfurter', 'Frankfurter')], max_length=255, verbose_name='Service Type'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -99,6 +99,7 @@ class ExchangeRateService(models.Model):
|
|||||||
COINGECKO_FREE = "coingecko_free", "CoinGecko (Demo/Free)"
|
COINGECKO_FREE = "coingecko_free", "CoinGecko (Demo/Free)"
|
||||||
COINGECKO_PRO = "coingecko_pro", "CoinGecko (Pro)"
|
COINGECKO_PRO = "coingecko_pro", "CoinGecko (Pro)"
|
||||||
TRANSITIVE = "transitive", "Transitive (Calculated from Existing Rates)"
|
TRANSITIVE = "transitive", "Transitive (Calculated from Existing Rates)"
|
||||||
|
FRANKFURTER = "frankfurter", "Frankfurter"
|
||||||
|
|
||||||
class IntervalType(models.TextChoices):
|
class IntervalType(models.TextChoices):
|
||||||
ON = "on", _("On")
|
ON = "on", _("On")
|
||||||
|
|||||||
Reference in New Issue
Block a user