django-cfg 1.2.29__py3-none-any.whl → 1.2.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/payments/admin/__init__.py +3 -2
- django_cfg/apps/payments/admin/balance_admin.py +18 -18
- django_cfg/apps/payments/admin/currencies_admin.py +319 -131
- django_cfg/apps/payments/admin/payments_admin.py +15 -4
- django_cfg/apps/payments/config/module.py +2 -2
- django_cfg/apps/payments/config/utils.py +2 -2
- django_cfg/apps/payments/decorators.py +2 -2
- django_cfg/apps/payments/management/commands/README.md +95 -127
- django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
- django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
- django_cfg/apps/payments/managers/__init__.py +3 -2
- django_cfg/apps/payments/managers/balance_manager.py +2 -2
- django_cfg/apps/payments/managers/currency_manager.py +272 -49
- django_cfg/apps/payments/managers/payment_manager.py +161 -13
- django_cfg/apps/payments/middleware/api_access.py +2 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
- django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
- django_cfg/apps/payments/models/__init__.py +3 -2
- django_cfg/apps/payments/models/currencies.py +187 -71
- django_cfg/apps/payments/models/payments.py +3 -2
- django_cfg/apps/payments/serializers/__init__.py +3 -2
- django_cfg/apps/payments/serializers/currencies.py +20 -12
- django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
- django_cfg/apps/payments/services/core/balance_service.py +2 -2
- django_cfg/apps/payments/services/core/fallback_service.py +2 -2
- django_cfg/apps/payments/services/core/payment_service.py +3 -6
- django_cfg/apps/payments/services/core/subscription_service.py +4 -7
- django_cfg/apps/payments/services/internal_types.py +171 -7
- django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
- django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
- django_cfg/apps/payments/services/providers/base.py +144 -43
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
- django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
- django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
- django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
- django_cfg/apps/payments/services/providers/registry.py +294 -11
- django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
- django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
- django_cfg/apps/payments/services/security/error_handler.py +6 -8
- django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
- django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
- django_cfg/apps/payments/signals/api_key_signals.py +2 -2
- django_cfg/apps/payments/signals/payment_signals.py +11 -5
- django_cfg/apps/payments/signals/subscription_signals.py +2 -2
- django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
- django_cfg/apps/payments/templates/payments/base.html +4 -4
- django_cfg/apps/payments/templates/payments/components/payment_card.html +6 -6
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +4 -4
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +14 -7
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +2 -2
- django_cfg/apps/payments/templates/payments/components/status_badge.html +8 -1
- django_cfg/apps/payments/templates/payments/components/status_overview.html +34 -30
- django_cfg/apps/payments/templates/payments/dashboard.html +202 -290
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
- django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
- django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
- django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
- django_cfg/apps/payments/templates/payments/stats.html +261 -0
- django_cfg/apps/payments/templates/payments/test.html +213 -0
- django_cfg/apps/payments/urls.py +3 -1
- django_cfg/apps/payments/{urls_templates.py → urls_admin.py} +6 -0
- django_cfg/apps/payments/utils/__init__.py +1 -3
- django_cfg/apps/payments/utils/billing_utils.py +2 -2
- django_cfg/apps/payments/utils/config_utils.py +2 -8
- django_cfg/apps/payments/utils/validation_utils.py +2 -2
- django_cfg/apps/payments/views/__init__.py +3 -2
- django_cfg/apps/payments/views/currency_views.py +31 -20
- django_cfg/apps/payments/views/payment_views.py +2 -2
- django_cfg/apps/payments/views/templates/ajax.py +141 -2
- django_cfg/apps/payments/views/templates/base.py +21 -13
- django_cfg/apps/payments/views/templates/payment_detail.py +1 -1
- django_cfg/apps/payments/views/templates/payment_management.py +34 -40
- django_cfg/apps/payments/views/templates/stats.py +8 -4
- django_cfg/apps/payments/views/webhook_views.py +2 -2
- django_cfg/apps/payments/viewsets.py +3 -2
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +4 -4
- django_cfg/core/config.py +35 -0
- django_cfg/models/payments.py +2 -8
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_unfold/dashboard.py +7 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/METADATA +2 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/RECORD +118 -96
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,289 @@
|
|
1
|
+
"""
|
2
|
+
CoinPaprika client for crypto rates - much simpler and more reliable than CoinGecko.
|
3
|
+
|
4
|
+
CoinPaprika API provides all crypto rates in a single request without rate limits.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import time
|
9
|
+
import requests
|
10
|
+
from datetime import datetime
|
11
|
+
from typing import Dict, Set, Optional, List
|
12
|
+
from cachetools import TTLCache
|
13
|
+
|
14
|
+
from ..core.models import Rate, CoinPaprikaTicker, CoinPaprikaTickersResponse
|
15
|
+
from ..core.exceptions import RateFetchError
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class CoinPaprikaClient:
|
21
|
+
"""Client for fetching crypto rates from CoinPaprika API."""
|
22
|
+
|
23
|
+
def __init__(self, cache_ttl: int = 600):
|
24
|
+
"""Initialize CoinPaprika client with TTL cache."""
|
25
|
+
self.base_url = "https://api.coinpaprika.com/v1"
|
26
|
+
self._rate_cache = TTLCache(maxsize=5000, ttl=cache_ttl) # Cache rates for 10 minutes
|
27
|
+
self._all_rates_cache = TTLCache(maxsize=1, ttl=300) # Cache all rates for 5 minutes
|
28
|
+
self._session = requests.Session()
|
29
|
+
self._session.headers.update({
|
30
|
+
'User-Agent': 'django-cfg-currency-client/1.0',
|
31
|
+
'Accept': 'application/json'
|
32
|
+
})
|
33
|
+
|
34
|
+
def fetch_rate(self, base: str, quote: str) -> Rate:
|
35
|
+
"""
|
36
|
+
Fetch crypto exchange rate from CoinPaprika.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
base: Base currency code (crypto)
|
40
|
+
quote: Quote currency code (usually USD)
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
Rate object with exchange rate data
|
44
|
+
|
45
|
+
Raises:
|
46
|
+
RateFetchError: If rate fetch fails
|
47
|
+
"""
|
48
|
+
if quote.upper() != 'USD':
|
49
|
+
raise RateFetchError(f"CoinPaprika only supports USD quotes, got {quote}")
|
50
|
+
|
51
|
+
cache_key = f"{base.upper()}_{quote.upper()}"
|
52
|
+
|
53
|
+
# Try cache first
|
54
|
+
if cache_key in self._rate_cache:
|
55
|
+
logger.debug(f"Retrieved rate {base}/{quote} from cache")
|
56
|
+
return self._rate_cache[cache_key]
|
57
|
+
|
58
|
+
try:
|
59
|
+
# Get all rates and find our currency
|
60
|
+
all_rates = self._fetch_all_rates()
|
61
|
+
|
62
|
+
base_upper = base.upper()
|
63
|
+
for ticker in all_rates:
|
64
|
+
if ticker.symbol == base_upper:
|
65
|
+
price = ticker.quotes.USD.price
|
66
|
+
|
67
|
+
# Parse ISO format: 2021-01-01T00:00:00Z
|
68
|
+
timestamp = datetime.fromisoformat(ticker.last_updated.replace('Z', '+00:00'))
|
69
|
+
|
70
|
+
rate = Rate(
|
71
|
+
source="coinpaprika",
|
72
|
+
base_currency=base.upper(),
|
73
|
+
quote_currency="USD",
|
74
|
+
rate=float(price),
|
75
|
+
timestamp=timestamp
|
76
|
+
)
|
77
|
+
|
78
|
+
# Cache the result
|
79
|
+
self._rate_cache[cache_key] = rate
|
80
|
+
|
81
|
+
return rate
|
82
|
+
|
83
|
+
raise RateFetchError(f"Currency {base} not found in CoinPaprika data")
|
84
|
+
|
85
|
+
except Exception as e:
|
86
|
+
logger.error(f"Failed to fetch rate for {base}/{quote}: {e}")
|
87
|
+
raise RateFetchError(f"CoinPaprika fetch failed: {e}")
|
88
|
+
|
89
|
+
def _fetch_all_tickers(self) -> Dict[str, dict]:
|
90
|
+
"""
|
91
|
+
Fetch all tickers from CoinPaprika API.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
Dict with symbol as key and ticker data as value
|
95
|
+
"""
|
96
|
+
cache_key = "all_tickers"
|
97
|
+
if cache_key in self._all_rates_cache:
|
98
|
+
logger.debug("Retrieved all tickers from CoinPaprika cache")
|
99
|
+
return self._all_rates_cache[cache_key]
|
100
|
+
|
101
|
+
try:
|
102
|
+
response = requests.get(f"{self.base_url}/tickers")
|
103
|
+
response.raise_for_status()
|
104
|
+
tickers_data = response.json()
|
105
|
+
|
106
|
+
# Process data into a more accessible format: {symbol: {id: ..., price: ...}}
|
107
|
+
processed_tickers = {}
|
108
|
+
for ticker in tickers_data:
|
109
|
+
symbol = ticker['symbol'].upper()
|
110
|
+
processed_tickers[symbol] = {
|
111
|
+
'id': ticker['id'],
|
112
|
+
'name': ticker['name'],
|
113
|
+
'price_usd': ticker['quotes']['USD']['price'] if 'USD' in ticker['quotes'] else None,
|
114
|
+
'last_updated': ticker['last_updated']
|
115
|
+
}
|
116
|
+
|
117
|
+
self._all_rates_cache[cache_key] = processed_tickers
|
118
|
+
logger.info(f"Fetched and cached {len(processed_tickers)} tickers from CoinPaprika")
|
119
|
+
return processed_tickers
|
120
|
+
except requests.exceptions.RequestException as e:
|
121
|
+
logger.error(f"Failed to fetch all tickers from CoinPaprika: {e}")
|
122
|
+
raise RateFetchError(f"CoinPaprika API error: {e}")
|
123
|
+
|
124
|
+
def _fetch_all_rates(self) -> List[CoinPaprikaTicker]:
|
125
|
+
"""
|
126
|
+
Fetch all cryptocurrency rates from CoinPaprika.
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
List of CoinPaprikaTicker objects
|
130
|
+
"""
|
131
|
+
cache_key = "all_rates"
|
132
|
+
|
133
|
+
# Try cache first
|
134
|
+
if cache_key in self._all_rates_cache:
|
135
|
+
logger.debug("Retrieved all rates from cache")
|
136
|
+
return self._all_rates_cache[cache_key]
|
137
|
+
|
138
|
+
try:
|
139
|
+
url = f"{self.base_url}/tickers"
|
140
|
+
logger.debug(f"Fetching all rates from {url}")
|
141
|
+
|
142
|
+
response = self._session.get(url, timeout=30)
|
143
|
+
response.raise_for_status()
|
144
|
+
|
145
|
+
raw_data = response.json()
|
146
|
+
|
147
|
+
# Validate response using Pydantic model
|
148
|
+
try:
|
149
|
+
tickers_response = CoinPaprikaTickersResponse(raw_data)
|
150
|
+
tickers = tickers_response.root
|
151
|
+
except Exception as e:
|
152
|
+
raise RateFetchError(f"Invalid CoinPaprika response format: {e}")
|
153
|
+
|
154
|
+
# Cache the result
|
155
|
+
self._all_rates_cache[cache_key] = tickers
|
156
|
+
logger.info(f"Fetched {len(tickers)} cryptocurrencies from CoinPaprika")
|
157
|
+
|
158
|
+
return tickers
|
159
|
+
|
160
|
+
except requests.RequestException as e:
|
161
|
+
logger.error(f"HTTP error fetching from CoinPaprika: {e}")
|
162
|
+
raise RateFetchError(f"Failed to fetch data from CoinPaprika: {e}")
|
163
|
+
except Exception as e:
|
164
|
+
logger.error(f"Unexpected error fetching from CoinPaprika: {e}")
|
165
|
+
raise RateFetchError(f"CoinPaprika fetch failed: {e}")
|
166
|
+
|
167
|
+
def get_supported_cryptocurrencies(self) -> Set[str]:
|
168
|
+
"""
|
169
|
+
Get all supported cryptocurrency symbols.
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
Set of supported crypto symbols
|
173
|
+
"""
|
174
|
+
try:
|
175
|
+
all_rates = self._fetch_all_rates()
|
176
|
+
symbols = {ticker.symbol for ticker in all_rates}
|
177
|
+
logger.debug(f"Found {len(symbols)} supported cryptocurrencies")
|
178
|
+
return symbols
|
179
|
+
except Exception as e:
|
180
|
+
logger.error(f"Failed to get supported cryptocurrencies: {e}")
|
181
|
+
return set()
|
182
|
+
|
183
|
+
def get_all_supported_currencies(self) -> Dict[str, str]:
|
184
|
+
"""Get all supported cryptocurrencies from CoinPaprika."""
|
185
|
+
all_tickers = self._fetch_all_tickers()
|
186
|
+
return {symbol: data['name'] for symbol, data in all_tickers.items() if data['price_usd'] is not None}
|
187
|
+
|
188
|
+
def supports_pair(self, base: str, quote: str) -> bool:
|
189
|
+
"""
|
190
|
+
Check if a currency pair is supported.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
base: Base currency code
|
194
|
+
quote: Quote currency code
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
True if supported, False otherwise
|
198
|
+
"""
|
199
|
+
if quote.upper() != 'USD':
|
200
|
+
return False
|
201
|
+
|
202
|
+
supported_cryptos = self.get_supported_cryptocurrencies()
|
203
|
+
return base.upper() in supported_cryptos
|
204
|
+
|
205
|
+
def fetch_multiple_rates(self, currency_codes: List[str], quote: str = 'USD') -> Dict[str, Rate]:
|
206
|
+
"""
|
207
|
+
Fetch multiple cryptocurrency rates efficiently.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
currency_codes: List of crypto currency codes
|
211
|
+
quote: Quote currency (default: USD)
|
212
|
+
|
213
|
+
Returns:
|
214
|
+
Dictionary mapping currency codes to Rate objects
|
215
|
+
"""
|
216
|
+
if quote.upper() != 'USD':
|
217
|
+
raise RateFetchError(f"CoinPaprika only supports USD quotes, got {quote}")
|
218
|
+
|
219
|
+
results = {}
|
220
|
+
|
221
|
+
try:
|
222
|
+
# Fetch all rates once
|
223
|
+
all_rates = self._fetch_all_rates()
|
224
|
+
|
225
|
+
# Create lookup dictionary
|
226
|
+
rates_by_symbol = {ticker.symbol: ticker for ticker in all_rates}
|
227
|
+
|
228
|
+
# Process requested currencies
|
229
|
+
for currency_code in currency_codes:
|
230
|
+
currency_upper = currency_code.upper()
|
231
|
+
|
232
|
+
if currency_upper in rates_by_symbol:
|
233
|
+
ticker = rates_by_symbol[currency_upper]
|
234
|
+
price = ticker.quotes.USD.price
|
235
|
+
|
236
|
+
# Parse ISO format: 2021-01-01T00:00:00Z
|
237
|
+
timestamp = datetime.fromisoformat(ticker.last_updated.replace('Z', '+00:00'))
|
238
|
+
|
239
|
+
rate = Rate(
|
240
|
+
source="coinpaprika",
|
241
|
+
base_currency=currency_upper,
|
242
|
+
quote_currency="USD",
|
243
|
+
rate=float(price),
|
244
|
+
timestamp=timestamp
|
245
|
+
)
|
246
|
+
|
247
|
+
results[currency_upper] = rate
|
248
|
+
|
249
|
+
# Cache individual rate
|
250
|
+
cache_key = f"{currency_upper}_USD"
|
251
|
+
self._rate_cache[cache_key] = rate
|
252
|
+
else:
|
253
|
+
logger.warning(f"Currency {currency_code} not found in CoinPaprika data")
|
254
|
+
|
255
|
+
logger.info(f"Successfully fetched {len(results)} rates from CoinPaprika")
|
256
|
+
return results
|
257
|
+
|
258
|
+
except Exception as e:
|
259
|
+
logger.error(f"Failed to fetch multiple rates: {e}")
|
260
|
+
raise RateFetchError(f"CoinPaprika batch fetch failed: {e}")
|
261
|
+
|
262
|
+
def get_top_cryptocurrencies(self, limit: int = 100) -> List[Dict]:
|
263
|
+
"""
|
264
|
+
Get top cryptocurrencies by market cap rank.
|
265
|
+
|
266
|
+
Args:
|
267
|
+
limit: Maximum number of currencies to return
|
268
|
+
|
269
|
+
Returns:
|
270
|
+
List of cryptocurrency data dictionaries
|
271
|
+
"""
|
272
|
+
try:
|
273
|
+
all_rates = self._fetch_all_rates()
|
274
|
+
|
275
|
+
# Filter and sort by rank
|
276
|
+
valid_tickers = [
|
277
|
+
ticker for ticker in all_rates
|
278
|
+
if ticker.rank and ticker.quotes.USD.price
|
279
|
+
]
|
280
|
+
|
281
|
+
# Sort by rank and limit
|
282
|
+
top_tickers = sorted(valid_tickers, key=lambda x: x.rank)[:limit]
|
283
|
+
|
284
|
+
logger.info(f"Retrieved top {len(top_tickers)} cryptocurrencies")
|
285
|
+
return top_tickers
|
286
|
+
|
287
|
+
except Exception as e:
|
288
|
+
logger.error(f"Failed to get top cryptocurrencies: {e}")
|
289
|
+
return []
|
@@ -0,0 +1,157 @@
|
|
1
|
+
import logging
|
2
|
+
import requests
|
3
|
+
import time
|
4
|
+
from datetime import datetime
|
5
|
+
from typing import Dict, Set, Optional
|
6
|
+
from cachetools import TTLCache
|
7
|
+
|
8
|
+
from ..core.models import Rate, YahooFinanceResponse
|
9
|
+
from ..core.exceptions import RateFetchError
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
class YahooFinanceClient:
|
15
|
+
"""Simple Yahoo Finance client without yfinance dependency."""
|
16
|
+
|
17
|
+
BASE_URL = "https://query1.finance.yahoo.com/v8/finance/chart"
|
18
|
+
|
19
|
+
def __init__(self, cache_ttl: int = 3600):
|
20
|
+
"""Initialize Yahoo Finance client with TTL cache."""
|
21
|
+
self._rate_cache = TTLCache(maxsize=500, ttl=cache_ttl)
|
22
|
+
self._session = requests.Session()
|
23
|
+
self._session.headers.update({
|
24
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
25
|
+
})
|
26
|
+
self._last_request_time = 0
|
27
|
+
self._rate_limit_delay = 1.0 # 1 second between requests
|
28
|
+
|
29
|
+
def _get_yahoo_symbol(self, base: str, quote: str) -> str:
|
30
|
+
"""Convert currency pair to Yahoo Finance symbol format."""
|
31
|
+
# Yahoo uses format like EURUSD=X for forex pairs
|
32
|
+
return f"{base}{quote}=X"
|
33
|
+
|
34
|
+
def fetch_rate(self, base: str, quote: str) -> Rate:
|
35
|
+
"""
|
36
|
+
Fetch forex rate from Yahoo Finance with caching.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
base: Base currency code (e.g., EUR)
|
40
|
+
quote: Quote currency code (e.g., USD)
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
Rate object with exchange rate data
|
44
|
+
|
45
|
+
Raises:
|
46
|
+
RateFetchError: If rate fetch fails
|
47
|
+
"""
|
48
|
+
base = base.upper()
|
49
|
+
quote = quote.upper()
|
50
|
+
cache_key = f"{base}_{quote}"
|
51
|
+
|
52
|
+
# Try cache first
|
53
|
+
if cache_key in self._rate_cache:
|
54
|
+
logger.debug(f"Retrieved rate {base}/{quote} from Yahoo cache")
|
55
|
+
return self._rate_cache[cache_key]
|
56
|
+
|
57
|
+
symbol = self._get_yahoo_symbol(base, quote)
|
58
|
+
|
59
|
+
# Rate limiting
|
60
|
+
current_time = time.time()
|
61
|
+
time_since_last_request = current_time - self._last_request_time
|
62
|
+
if time_since_last_request < self._rate_limit_delay:
|
63
|
+
sleep_time = self._rate_limit_delay - time_since_last_request
|
64
|
+
logger.debug(f"Rate limiting: sleeping for {sleep_time:.2f}s")
|
65
|
+
time.sleep(sleep_time)
|
66
|
+
|
67
|
+
try:
|
68
|
+
response = self._session.get(f"{self.BASE_URL}/{symbol}")
|
69
|
+
self._last_request_time = time.time()
|
70
|
+
response.raise_for_status()
|
71
|
+
|
72
|
+
raw_data = response.json()
|
73
|
+
|
74
|
+
# Validate response using Pydantic model
|
75
|
+
try:
|
76
|
+
yahoo_response = YahooFinanceResponse(**raw_data)
|
77
|
+
except Exception as e:
|
78
|
+
raise RateFetchError(f"Invalid Yahoo Finance response format: {e}")
|
79
|
+
|
80
|
+
if not yahoo_response.chart.result:
|
81
|
+
raise RateFetchError(f"No data returned for {symbol}")
|
82
|
+
|
83
|
+
meta = yahoo_response.chart.result[0].meta
|
84
|
+
rate_value = meta.regularMarketPrice
|
85
|
+
timestamp = datetime.fromtimestamp(meta.regularMarketTime)
|
86
|
+
|
87
|
+
rate = Rate(
|
88
|
+
source="yahoo",
|
89
|
+
base_currency=base,
|
90
|
+
quote_currency=quote,
|
91
|
+
rate=float(rate_value),
|
92
|
+
timestamp=timestamp
|
93
|
+
)
|
94
|
+
|
95
|
+
self._rate_cache[cache_key] = rate
|
96
|
+
logger.info(f"Fetched rate {base}/{quote} = {rate_value} from Yahoo Finance")
|
97
|
+
return rate
|
98
|
+
|
99
|
+
except requests.exceptions.RequestException as e:
|
100
|
+
logger.error(f"Failed to fetch rate from Yahoo Finance: {e}")
|
101
|
+
raise RateFetchError(f"Yahoo Finance API error: {e}")
|
102
|
+
except (KeyError, TypeError, ValueError) as e:
|
103
|
+
logger.error(f"Failed to parse Yahoo Finance response: {e}")
|
104
|
+
raise RateFetchError(f"Invalid response format: {e}")
|
105
|
+
except Exception as e:
|
106
|
+
logger.error(f"Unexpected error fetching from Yahoo Finance: {e}")
|
107
|
+
raise RateFetchError(f"Yahoo Finance fetch failed: {e}")
|
108
|
+
|
109
|
+
def supports_pair(self, base: str, quote: str) -> bool:
|
110
|
+
"""
|
111
|
+
Check if Yahoo Finance supports the given currency pair.
|
112
|
+
|
113
|
+
Yahoo Finance primarily supports major forex pairs.
|
114
|
+
"""
|
115
|
+
base = base.upper()
|
116
|
+
quote = quote.upper()
|
117
|
+
|
118
|
+
# Major currencies supported by Yahoo Finance
|
119
|
+
major_currencies = {
|
120
|
+
'USD', 'EUR', 'GBP', 'JPY', 'CHF', 'CAD', 'AUD', 'NZD',
|
121
|
+
'SEK', 'NOK', 'DKK', 'PLN', 'CZK', 'HUF', 'RUB', 'CNY',
|
122
|
+
'INR', 'KRW', 'SGD', 'HKD', 'THB', 'MXN', 'BRL', 'ZAR',
|
123
|
+
'TRY', 'ILS'
|
124
|
+
}
|
125
|
+
|
126
|
+
return base in major_currencies and quote in major_currencies
|
127
|
+
|
128
|
+
def get_all_supported_currencies(self) -> Dict[str, str]:
|
129
|
+
"""Get all major currencies supported by Yahoo Finance."""
|
130
|
+
return {
|
131
|
+
'USD': 'US Dollar',
|
132
|
+
'EUR': 'Euro',
|
133
|
+
'GBP': 'British Pound',
|
134
|
+
'JPY': 'Japanese Yen',
|
135
|
+
'CHF': 'Swiss Franc',
|
136
|
+
'CAD': 'Canadian Dollar',
|
137
|
+
'AUD': 'Australian Dollar',
|
138
|
+
'NZD': 'New Zealand Dollar',
|
139
|
+
'SEK': 'Swedish Krona',
|
140
|
+
'NOK': 'Norwegian Krone',
|
141
|
+
'DKK': 'Danish Krone',
|
142
|
+
'PLN': 'Polish Zloty',
|
143
|
+
'CZK': 'Czech Koruna',
|
144
|
+
'HUF': 'Hungarian Forint',
|
145
|
+
'RUB': 'Russian Ruble',
|
146
|
+
'CNY': 'Chinese Yuan',
|
147
|
+
'INR': 'Indian Rupee',
|
148
|
+
'KRW': 'South Korean Won',
|
149
|
+
'SGD': 'Singapore Dollar',
|
150
|
+
'HKD': 'Hong Kong Dollar',
|
151
|
+
'THB': 'Thai Baht',
|
152
|
+
'MXN': 'Mexican Peso',
|
153
|
+
'BRL': 'Brazilian Real',
|
154
|
+
'ZAR': 'South African Rand',
|
155
|
+
'TRY': 'Turkish Lira',
|
156
|
+
'ILS': 'Israeli Shekel'
|
157
|
+
}
|
@@ -5,10 +5,7 @@ Core currency conversion functionality.
|
|
5
5
|
from .models import (
|
6
6
|
Rate,
|
7
7
|
ConversionRequest,
|
8
|
-
ConversionResult
|
9
|
-
YFinanceCurrencies,
|
10
|
-
CoinGeckoCurrencies,
|
11
|
-
SupportedCurrencies
|
8
|
+
ConversionResult
|
12
9
|
)
|
13
10
|
|
14
11
|
from .exceptions import (
|
@@ -26,9 +23,6 @@ __all__ = [
|
|
26
23
|
'Rate',
|
27
24
|
'ConversionRequest',
|
28
25
|
'ConversionResult',
|
29
|
-
'YFinanceCurrencies',
|
30
|
-
'CoinGeckoCurrencies',
|
31
|
-
'SupportedCurrencies',
|
32
26
|
|
33
27
|
# Exceptions
|
34
28
|
'CurrencyError',
|
@@ -5,9 +5,9 @@ Main currency converter with intelligent routing.
|
|
5
5
|
import logging
|
6
6
|
from typing import Optional
|
7
7
|
|
8
|
-
from .models import Rate, ConversionRequest, ConversionResult
|
8
|
+
from .models import Rate, ConversionRequest, ConversionResult
|
9
9
|
from .exceptions import ConversionError, CurrencyNotFoundError
|
10
|
-
from ..clients import
|
10
|
+
from ..clients import YahooFinanceClient, CoinPaprikaClient
|
11
11
|
from ..utils.cache import CacheManager
|
12
12
|
|
13
13
|
logger = logging.getLogger(__name__)
|
@@ -23,8 +23,8 @@ class CurrencyConverter:
|
|
23
23
|
Args:
|
24
24
|
cache_ttl: Cache TTL in seconds
|
25
25
|
"""
|
26
|
-
self.
|
27
|
-
self.
|
26
|
+
self.yahoo = YahooFinanceClient(cache_ttl=cache_ttl)
|
27
|
+
self.coinpaprika = CoinPaprikaClient(cache_ttl=cache_ttl)
|
28
28
|
self.cache = CacheManager(ttl=cache_ttl)
|
29
29
|
|
30
30
|
def convert(self, amount: float, from_currency: str, to_currency: str) -> ConversionResult:
|
@@ -95,28 +95,28 @@ class CurrencyConverter:
|
|
95
95
|
CurrencyNotFoundError: If no provider supports the pair
|
96
96
|
"""
|
97
97
|
# Try cache first
|
98
|
-
for source in ["
|
98
|
+
for source in ["yahoo", "coinpaprika"]:
|
99
99
|
cached_rate = self.cache.get_rate(base, quote, source)
|
100
100
|
if cached_rate:
|
101
101
|
return cached_rate
|
102
102
|
|
103
|
-
# Try
|
104
|
-
if self.
|
103
|
+
# Try CoinPaprika first (excellent for crypto, no rate limits)
|
104
|
+
if self.coinpaprika.supports_pair(base, quote):
|
105
105
|
try:
|
106
|
-
rate = self.
|
106
|
+
rate = self.coinpaprika.fetch_rate(base, quote)
|
107
107
|
self.cache.set_rate(rate)
|
108
108
|
return rate
|
109
109
|
except Exception as e:
|
110
|
-
logger.warning(f"
|
110
|
+
logger.warning(f"CoinPaprika failed for {base}/{quote}: {e}")
|
111
111
|
|
112
|
-
# Try
|
113
|
-
if self.
|
112
|
+
# Try Yahoo Finance next (good for fiat and major forex pairs)
|
113
|
+
if self.yahoo.supports_pair(base, quote):
|
114
114
|
try:
|
115
|
-
rate = self.
|
115
|
+
rate = self.yahoo.fetch_rate(base, quote)
|
116
116
|
self.cache.set_rate(rate)
|
117
117
|
return rate
|
118
118
|
except Exception as e:
|
119
|
-
logger.warning(f"
|
119
|
+
logger.warning(f"Yahoo Finance failed for {base}/{quote}: {e}")
|
120
120
|
|
121
121
|
# Try indirect conversion via USD
|
122
122
|
if base != "USD" and quote != "USD":
|
@@ -156,14 +156,9 @@ class CurrencyConverter:
|
|
156
156
|
rate=combined_rate
|
157
157
|
)
|
158
158
|
|
159
|
-
def get_supported_currencies(self) ->
|
159
|
+
def get_supported_currencies(self) -> dict:
|
160
160
|
"""Get list of supported currencies by provider."""
|
161
|
-
return
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
coingecko=CoinGeckoCurrencies(
|
166
|
-
crypto=list(self.coingecko.get_crypto_ids().keys()),
|
167
|
-
vs_currencies=list(self.coingecko.get_vs_currencies())
|
168
|
-
)
|
169
|
-
)
|
161
|
+
return {
|
162
|
+
"yahoo": self.yahoo.get_all_supported_currencies(),
|
163
|
+
"coinpaprika": self.coinpaprika.get_all_supported_currencies()
|
164
|
+
}
|