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.
Files changed (126) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin/__init__.py +3 -2
  3. django_cfg/apps/payments/admin/balance_admin.py +18 -18
  4. django_cfg/apps/payments/admin/currencies_admin.py +319 -131
  5. django_cfg/apps/payments/admin/payments_admin.py +15 -4
  6. django_cfg/apps/payments/config/module.py +2 -2
  7. django_cfg/apps/payments/config/utils.py +2 -2
  8. django_cfg/apps/payments/decorators.py +2 -2
  9. django_cfg/apps/payments/management/commands/README.md +95 -127
  10. django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
  11. django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
  12. django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
  13. django_cfg/apps/payments/managers/__init__.py +3 -2
  14. django_cfg/apps/payments/managers/balance_manager.py +2 -2
  15. django_cfg/apps/payments/managers/currency_manager.py +272 -49
  16. django_cfg/apps/payments/managers/payment_manager.py +161 -13
  17. django_cfg/apps/payments/middleware/api_access.py +2 -2
  18. django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
  19. django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
  20. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
  21. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
  22. django_cfg/apps/payments/models/__init__.py +3 -2
  23. django_cfg/apps/payments/models/currencies.py +187 -71
  24. django_cfg/apps/payments/models/payments.py +3 -2
  25. django_cfg/apps/payments/serializers/__init__.py +3 -2
  26. django_cfg/apps/payments/serializers/currencies.py +20 -12
  27. django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
  28. django_cfg/apps/payments/services/core/balance_service.py +2 -2
  29. django_cfg/apps/payments/services/core/fallback_service.py +2 -2
  30. django_cfg/apps/payments/services/core/payment_service.py +3 -6
  31. django_cfg/apps/payments/services/core/subscription_service.py +4 -7
  32. django_cfg/apps/payments/services/internal_types.py +171 -7
  33. django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
  34. django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
  35. django_cfg/apps/payments/services/providers/base.py +144 -43
  36. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
  37. django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
  38. django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
  39. django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
  40. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
  41. django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
  42. django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
  43. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
  44. django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
  45. django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
  46. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
  47. django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
  48. django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
  49. django_cfg/apps/payments/services/providers/registry.py +294 -11
  50. django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
  51. django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
  52. django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
  53. django_cfg/apps/payments/services/security/error_handler.py +6 -8
  54. django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
  55. django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
  56. django_cfg/apps/payments/signals/api_key_signals.py +2 -2
  57. django_cfg/apps/payments/signals/payment_signals.py +11 -5
  58. django_cfg/apps/payments/signals/subscription_signals.py +2 -2
  59. django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
  60. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
  61. django_cfg/apps/payments/templates/payments/base.html +4 -4
  62. django_cfg/apps/payments/templates/payments/components/payment_card.html +6 -6
  63. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +4 -4
  64. django_cfg/apps/payments/templates/payments/components/progress_bar.html +14 -7
  65. django_cfg/apps/payments/templates/payments/components/provider_stats.html +2 -2
  66. django_cfg/apps/payments/templates/payments/components/status_badge.html +8 -1
  67. django_cfg/apps/payments/templates/payments/components/status_overview.html +34 -30
  68. django_cfg/apps/payments/templates/payments/dashboard.html +202 -290
  69. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
  70. django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
  71. django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
  72. django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
  73. django_cfg/apps/payments/templates/payments/stats.html +261 -0
  74. django_cfg/apps/payments/templates/payments/test.html +213 -0
  75. django_cfg/apps/payments/urls.py +3 -1
  76. django_cfg/apps/payments/{urls_templates.py → urls_admin.py} +6 -0
  77. django_cfg/apps/payments/utils/__init__.py +1 -3
  78. django_cfg/apps/payments/utils/billing_utils.py +2 -2
  79. django_cfg/apps/payments/utils/config_utils.py +2 -8
  80. django_cfg/apps/payments/utils/validation_utils.py +2 -2
  81. django_cfg/apps/payments/views/__init__.py +3 -2
  82. django_cfg/apps/payments/views/currency_views.py +31 -20
  83. django_cfg/apps/payments/views/payment_views.py +2 -2
  84. django_cfg/apps/payments/views/templates/ajax.py +141 -2
  85. django_cfg/apps/payments/views/templates/base.py +21 -13
  86. django_cfg/apps/payments/views/templates/payment_detail.py +1 -1
  87. django_cfg/apps/payments/views/templates/payment_management.py +34 -40
  88. django_cfg/apps/payments/views/templates/stats.py +8 -4
  89. django_cfg/apps/payments/views/webhook_views.py +2 -2
  90. django_cfg/apps/payments/viewsets.py +3 -2
  91. django_cfg/apps/tasks/urls.py +0 -2
  92. django_cfg/apps/tasks/urls_admin.py +14 -0
  93. django_cfg/apps/urls.py +4 -4
  94. django_cfg/core/config.py +35 -0
  95. django_cfg/models/payments.py +2 -8
  96. django_cfg/modules/django_currency/__init__.py +16 -11
  97. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  98. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  99. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  100. django_cfg/modules/django_currency/core/__init__.py +1 -7
  101. django_cfg/modules/django_currency/core/converter.py +18 -23
  102. django_cfg/modules/django_currency/core/models.py +122 -11
  103. django_cfg/modules/django_currency/database/__init__.py +4 -4
  104. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  105. django_cfg/modules/django_unfold/dashboard.py +7 -2
  106. django_cfg/template_archive/django_sample.zip +0 -0
  107. django_cfg/templates/admin/components/action_grid.html +9 -9
  108. django_cfg/templates/admin/components/metric_card.html +5 -5
  109. django_cfg/templates/admin/components/status_badge.html +2 -2
  110. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  111. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  112. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  113. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  114. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/METADATA +2 -4
  115. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/RECORD +118 -96
  116. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  117. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  118. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  119. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  120. django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
  121. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  122. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  123. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  124. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
  125. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
  126. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,5 @@
1
1
  """
2
- Database loader for populating currency data from external APIs.
2
+ Database loader for populating currency data using Yahoo Finance + CoinPaprika.
3
3
  """
4
4
 
5
5
  import logging
@@ -11,9 +11,8 @@ from dataclasses import dataclass
11
11
  from pydantic import BaseModel, Field, validator
12
12
  from cachetools import TTLCache
13
13
 
14
- # CoinGecko API
15
- from pycoingecko import CoinGeckoAPI
16
- import yfinance as yf
14
+ # Our new clients
15
+ from ..clients import YahooFinanceClient, CoinPaprikaClient
17
16
 
18
17
  logger = logging.getLogger(__name__)
19
18
 
@@ -22,14 +21,14 @@ logger = logging.getLogger(__name__)
22
21
  # PYDANTIC MODELS
23
22
  # ============================================================================
24
23
 
25
- class CoinGeckoCoinInfo(BaseModel):
26
- """Single coin information from CoinGecko."""
27
- id: str = Field(description="CoinGecko coin ID")
24
+ class CoinPaprikaCoinInfo(BaseModel):
25
+ """Single coin information from CoinPaprika."""
26
+ id: str = Field(description="CoinPaprika coin ID")
28
27
  symbol: str = Field(description="Currency symbol (e.g., BTC)")
29
28
  name: str = Field(description="Full coin name")
30
29
 
31
30
 
32
- class YFinanceCurrencyInfo(BaseModel):
31
+ class YahooFinanceCurrencyInfo(BaseModel):
33
32
  """Single fiat currency information."""
34
33
  code: str = Field(description="Currency code (e.g., USD)")
35
34
  name: str = Field(description="Full currency name")
@@ -45,404 +44,285 @@ class CurrencyRateInfo(BaseModel):
45
44
  decimal_places: int = Field(default=2, description="Decimal places")
46
45
  usd_rate: float = Field(description="Rate to USD")
47
46
  min_payment_amount: float = Field(default=1.0, description="Minimum payment amount")
48
- is_active: bool = Field(default=True, description="Is currency active")
49
47
 
50
48
 
51
- class DatabaseLoaderConfig(BaseModel):
52
- """Configuration for database loader."""
53
-
54
- # Rate limiting
55
- coingecko_delay: float = Field(default=1.5, description="Delay between CoinGecko requests (seconds)")
56
- yfinance_delay: float = Field(default=0.5, description="Delay between YFinance requests (seconds)")
57
- max_requests_per_minute: int = Field(default=30, description="Max requests per minute")
58
-
59
- # Limits
60
- max_cryptocurrencies: int = Field(default=500, description="Max cryptocurrencies to load")
61
- max_fiat_currencies: int = Field(default=50, description="Max fiat currencies to load")
62
-
63
- # Filtering
64
- min_market_cap_usd: float = Field(default=1000000, description="Minimum market cap in USD")
65
- exclude_stablecoins: bool = Field(default=False, description="Exclude stablecoins")
66
-
67
- # Cache
68
- cache_ttl_hours: int = Field(default=24, description="Cache TTL in hours")
69
-
70
-
71
- # ============================================================================
72
- # RATE LIMITER
73
- # ============================================================================
74
-
75
- @dataclass
76
49
  class RateLimiter:
77
- """Simple rate limiter to prevent API throttling."""
50
+ """Simple rate limiter for API calls."""
78
51
 
79
52
  def __init__(self, max_requests_per_minute: int = 30):
80
53
  self.max_requests = max_requests_per_minute
81
- self.requests = []
82
- self.last_request_time = 0.0
83
-
84
- def wait_if_needed(self, delay: float = 1.0):
54
+ self.request_times = []
55
+
56
+ def __call__(self):
85
57
  """Wait if necessary to respect rate limits."""
86
- current_time = time.time()
58
+ now = time.time()
87
59
 
88
- # Remove old requests (older than 1 minute)
89
- self.requests = [req_time for req_time in self.requests
90
- if current_time - req_time < 60]
60
+ # Remove requests older than 1 minute
61
+ self.request_times = [t for t in self.request_times if now - t < 60]
91
62
 
92
- # Check if we've hit the rate limit
93
- if len(self.requests) >= self.max_requests:
94
- sleep_time = 60 - (current_time - self.requests[0])
63
+ # If we're at the limit, wait
64
+ if len(self.request_times) >= self.max_requests:
65
+ sleep_time = 60 - (now - self.request_times[0]) + 1
95
66
  if sleep_time > 0:
96
- logger.info(f"Rate limit reached, sleeping for {sleep_time:.2f}s")
67
+ logger.debug(f"Rate limiting: sleeping for {sleep_time:.1f}s")
97
68
  time.sleep(sleep_time)
98
-
99
- # Check minimum delay between requests
100
- time_since_last = current_time - self.last_request_time
101
- if time_since_last < delay:
102
- sleep_time = delay - time_since_last
103
- logger.debug(f"Rate limiting: sleeping for {sleep_time:.2f}s")
104
- time.sleep(sleep_time)
105
-
106
- # Record this request
107
- self.requests.append(time.time())
108
- self.last_request_time = time.time()
69
+
70
+ self.request_times.append(now)
109
71
 
110
72
 
111
- # ============================================================================
112
- # DATABASE LOADER
113
- # ============================================================================
73
+ class DatabaseLoaderConfig(BaseModel):
74
+ """Configuration for database loader."""
75
+
76
+ yahoo_delay: float = Field(default=1.0, description="Delay between Yahoo requests")
77
+ coinpaprika_delay: float = Field(default=0.5, description="Delay between CoinPaprika requests")
78
+ max_requests_per_minute: int = Field(default=30, description="Max requests per minute")
79
+ max_cryptocurrencies: int = Field(default=500, description="Max cryptocurrencies to load")
80
+ max_fiat_currencies: int = Field(default=50, description="Max fiat currencies to load")
81
+ min_market_cap_usd: float = Field(default=1000000, description="Minimum market cap for crypto")
82
+ exclude_stablecoins: bool = Field(default=False, description="Exclude stablecoins")
83
+ cache_ttl_hours: int = Field(default=24, description="Cache TTL in hours")
84
+
114
85
 
115
86
  class CurrencyDatabaseLoader:
116
87
  """
117
- Typed tool for loading currency data into database.
118
-
119
- Features:
120
- - Rate limiting to prevent API throttling
121
- - Configurable limits on number of currencies
122
- - Market cap filtering for cryptocurrencies
123
- - Caching to avoid repeated API calls
124
- - Type safety with Pydantic models
88
+ Database loader for populating currency data from Yahoo Finance and CoinPaprika.
125
89
  """
126
90
 
127
91
  def __init__(self, config: DatabaseLoaderConfig = None):
128
92
  """Initialize the database loader."""
129
93
  self.config = config or DatabaseLoaderConfig()
130
94
 
131
- # Initialize API clients
132
- self.coingecko = CoinGeckoAPI()
95
+ # Initialize clients
96
+ self.yahoo = YahooFinanceClient(cache_ttl=self.config.cache_ttl_hours * 3600)
97
+ self.coinpaprika = CoinPaprikaClient(cache_ttl=self.config.cache_ttl_hours * 3600)
133
98
 
134
99
  # Rate limiters
135
- self.coingecko_limiter = RateLimiter(self.config.max_requests_per_minute)
136
- self.yfinance_limiter = RateLimiter(self.config.max_requests_per_minute)
100
+ self.yahoo_limiter = RateLimiter(self.config.max_requests_per_minute)
101
+ self.coinpaprika_limiter = RateLimiter(self.config.max_requests_per_minute)
137
102
 
138
- # Cache
103
+ # Caches
139
104
  cache_ttl = self.config.cache_ttl_hours * 3600
140
- self.crypto_cache = TTLCache(maxsize=10, ttl=cache_ttl)
141
- self.fiat_cache = TTLCache(maxsize=10, ttl=cache_ttl)
105
+ self.crypto_cache = TTLCache(maxsize=1000, ttl=cache_ttl)
106
+ self.fiat_cache = TTLCache(maxsize=100, ttl=cache_ttl)
142
107
 
143
108
  logger.info(f"Initialized CurrencyDatabaseLoader with config: {self.config}")
144
109
 
145
- def get_cryptocurrency_list(self) -> List[CoinGeckoCoinInfo]:
110
+ def get_fiat_currency_list(self) -> List[YahooFinanceCurrencyInfo]:
146
111
  """
147
- Get list of cryptocurrencies from CoinGecko with filtering.
112
+ Get list of supported fiat currencies.
148
113
 
149
114
  Returns:
150
- List of cryptocurrency information
151
- """
152
- cache_key = "crypto_list"
153
- if cache_key in self.crypto_cache:
154
- logger.debug("Retrieved cryptocurrency list from cache")
155
- return self.crypto_cache[cache_key]
156
-
157
- logger.info("Fetching cryptocurrency list from CoinGecko...")
158
-
159
- try:
160
- # Get coins with market data for filtering
161
- self.coingecko_limiter.wait_if_needed(self.config.coingecko_delay)
162
-
163
- coins_markets = self.coingecko.get_coins_markets(
164
- vs_currency='usd',
165
- order='market_cap_desc',
166
- per_page=self.config.max_cryptocurrencies,
167
- page=1,
168
- sparkline=False
169
- )
170
-
171
- cryptocurrencies = []
172
- for coin in coins_markets:
173
- # Filter by market cap
174
- market_cap = coin.get('market_cap', 0) or 0
175
- if market_cap < self.config.min_market_cap_usd:
176
- continue
177
-
178
- # Filter stablecoins if requested
179
- if self.config.exclude_stablecoins:
180
- categories = coin.get('categories', []) or []
181
- if any('stablecoin' in str(cat).lower() for cat in categories):
182
- continue
183
-
184
- crypto_info = CoinGeckoCoinInfo(
185
- id=coin['id'],
186
- symbol=coin['symbol'].upper(),
187
- name=coin['name']
188
- )
189
- cryptocurrencies.append(crypto_info)
190
-
191
- logger.info(f"Loaded {len(cryptocurrencies)} cryptocurrencies")
192
- self.crypto_cache[cache_key] = cryptocurrencies
193
- return cryptocurrencies
194
-
195
- except Exception as e:
196
- logger.error(f"Failed to fetch cryptocurrency list: {e}")
197
- raise
198
-
199
- def get_fiat_currency_list(self) -> List[YFinanceCurrencyInfo]:
115
+ List of fiat currency info objects
200
116
  """
201
- Get list of fiat currencies.
117
+ cache_key = "fiat_currencies"
202
118
 
203
- Returns:
204
- List of fiat currency information
205
- """
206
- cache_key = "fiat_list"
207
119
  if cache_key in self.fiat_cache:
208
- logger.debug("Retrieved fiat currency list from cache")
120
+ logger.debug("Retrieved fiat currencies from cache")
209
121
  return self.fiat_cache[cache_key]
210
122
 
211
- logger.info("Building fiat currency list...")
212
-
213
- # Major fiat currencies with their symbols
214
- fiat_currencies_data = [
215
- ("USD", "US Dollar", "$"),
216
- ("EUR", "Euro", "€"),
217
- ("GBP", "British Pound", "£"),
218
- ("JPY", "Japanese Yen", "¥"),
219
- ("CNY", "Chinese Yuan", "¥"),
220
- ("KRW", "South Korean Won", "₩"),
221
- ("CAD", "Canadian Dollar", "C$"),
222
- ("AUD", "Australian Dollar", "A$"),
223
- ("CHF", "Swiss Franc", "₣"),
224
- ("RUB", "Russian Ruble", "₽"),
225
- ("BRL", "Brazilian Real", "R$"),
226
- ("INR", "Indian Rupee", "₹"),
227
- ("MXN", "Mexican Peso", "$"),
228
- ("SGD", "Singapore Dollar", "S$"),
229
- ("HKD", "Hong Kong Dollar", "HK$"),
230
- ("SEK", "Swedish Krona", "kr"),
231
- ("NOK", "Norwegian Krone", "kr"),
232
- ("DKK", "Danish Krone", "kr"),
233
- ("PLN", "Polish Zloty", "zł"),
234
- ("CZK", "Czech Koruna", "Kč"),
235
- ("HUF", "Hungarian Forint", "Ft"),
236
- ("TRY", "Turkish Lira", "₺"),
237
- ("ZAR", "South African Rand", "R"),
238
- ("THB", "Thai Baht", "฿"),
239
- ("MYR", "Malaysian Ringgit", "RM"),
240
- ("PHP", "Philippine Peso", "₱"),
241
- ("IDR", "Indonesian Rupiah", "Rp"),
242
- ("VND", "Vietnamese Dong", "₫"),
243
- ("TWD", "Taiwan Dollar", "NT$"),
244
- ("NZD", "New Zealand Dollar", "NZ$")
245
- ]
123
+ # Get supported currencies from Yahoo
124
+ supported_currencies = self.yahoo.get_all_supported_currencies()
246
125
 
126
+ # Convert to our format and limit count
247
127
  fiat_currencies = []
248
- for code, name, symbol in fiat_currencies_data[:self.config.max_fiat_currencies]:
249
- fiat_info = YFinanceCurrencyInfo(
128
+ for code, name in list(supported_currencies.items())[:self.config.max_fiat_currencies]:
129
+ currency_info = YahooFinanceCurrencyInfo(
250
130
  code=code,
251
131
  name=name,
252
- symbol=symbol
132
+ symbol="" # Yahoo doesn't provide symbols
253
133
  )
254
- fiat_currencies.append(fiat_info)
134
+ fiat_currencies.append(currency_info)
255
135
 
256
- logger.info(f"Built list of {len(fiat_currencies)} fiat currencies")
257
136
  self.fiat_cache[cache_key] = fiat_currencies
137
+ logger.info(f"Loaded {len(fiat_currencies)} fiat currencies")
138
+
258
139
  return fiat_currencies
259
140
 
260
- def get_currency_rates(self, currencies: List[str], vs_currency: str = "usd") -> Dict[str, float]:
141
+ def get_cryptocurrency_list(self) -> List[CoinPaprikaCoinInfo]:
261
142
  """
262
- Get current rates for multiple currencies.
143
+ Get list of supported cryptocurrencies from CoinPaprika.
263
144
 
264
- Args:
265
- currencies: List of currency codes/IDs
266
- vs_currency: Quote currency (default: usd)
267
-
268
145
  Returns:
269
- Dictionary mapping currency to its rate
146
+ List of cryptocurrency info objects
270
147
  """
271
- if not currencies:
272
- return {}
148
+ cache_key = "crypto_currencies"
273
149
 
274
- logger.info(f"Fetching rates for {len(currencies)} currencies vs {vs_currency}")
150
+ if cache_key in self.crypto_cache:
151
+ logger.debug("Retrieved cryptocurrencies from cache")
152
+ return self.crypto_cache[cache_key]
275
153
 
276
154
  try:
277
- # Split into chunks to avoid hitting API limits
278
- chunk_size = 50
279
- all_rates = {}
155
+ # Get all tickers from CoinPaprika
156
+ all_tickers = self.coinpaprika._fetch_all_tickers()
280
157
 
281
- for i in range(0, len(currencies), chunk_size):
282
- chunk = currencies[i:i + chunk_size]
283
-
284
- self.coingecko_limiter.wait_if_needed(self.config.coingecko_delay)
158
+ crypto_currencies = []
159
+ for symbol, data in all_tickers.items():
160
+ # Skip if no price data
161
+ if data.get('price_usd') is None:
162
+ continue
285
163
 
286
- # Join currency IDs for batch request
287
- ids_str = ','.join(chunk)
164
+ # Apply market cap filter if needed
165
+ # Note: CoinPaprika doesn't provide market cap in tickers endpoint
166
+ # We'd need to use a different endpoint for that
288
167
 
289
- price_data = self.coingecko.get_price(
290
- ids=ids_str,
291
- vs_currencies=vs_currency,
292
- include_last_updated_at=True
168
+ coin_info = CoinPaprikaCoinInfo(
169
+ id=data['id'],
170
+ symbol=symbol,
171
+ name=data['name']
293
172
  )
173
+ crypto_currencies.append(coin_info)
294
174
 
295
- # Extract rates
296
- for currency_id, data in price_data.items():
297
- if vs_currency in data:
298
- all_rates[currency_id] = float(data[vs_currency])
299
-
300
- logger.debug(f"Fetched rates for chunk {i//chunk_size + 1}")
175
+ # Limit count
176
+ if len(crypto_currencies) >= self.config.max_cryptocurrencies:
177
+ break
178
+
179
+ self.crypto_cache[cache_key] = crypto_currencies
180
+ logger.info(f"Loaded {len(crypto_currencies)} cryptocurrencies")
301
181
 
302
- logger.info(f"Successfully fetched {len(all_rates)} currency rates")
303
- return all_rates
182
+ return crypto_currencies
304
183
 
305
184
  except Exception as e:
306
- logger.error(f"Failed to fetch currency rates: {e}")
307
- raise
185
+ logger.error(f"Failed to get cryptocurrency list: {e}")
186
+ return []
308
187
 
309
- def get_fiat_rate(self, base_currency: str, quote_currency: str = "USD") -> Optional[float]:
188
+ def get_currency_rates(self, currency_ids: List[str], quote: str = 'usd') -> Dict[str, float]:
310
189
  """
311
- Get fiat currency rate using YFinance.
190
+ Get current rates for multiple cryptocurrencies.
312
191
 
313
192
  Args:
314
- base_currency: Base currency code
315
- quote_currency: Quote currency code
193
+ currency_ids: List of currency symbols
194
+ quote: Quote currency (usually 'usd')
316
195
 
317
196
  Returns:
318
- Exchange rate or None if not available
197
+ Dict mapping currency ID to rate
319
198
  """
199
+ self.coinpaprika_limiter()
200
+
320
201
  try:
321
- self.yfinance_limiter.wait_if_needed(self.config.yfinance_delay)
202
+ rates = {}
203
+ all_tickers = self.coinpaprika._fetch_all_tickers()
322
204
 
323
- if base_currency == quote_currency:
324
- return 1.0
205
+ for currency_id in currency_ids:
206
+ if currency_id.upper() in all_tickers:
207
+ price = all_tickers[currency_id.upper()].get('price_usd')
208
+ if price is not None:
209
+ rates[currency_id] = price
325
210
 
326
- symbol = f"{base_currency}{quote_currency}=X"
327
- ticker = yf.Ticker(symbol)
211
+ logger.debug(f"Retrieved rates for {len(rates)} currencies")
212
+ return rates
328
213
 
329
- # Try to get current price
330
- info = ticker.info
331
- if 'regularMarketPrice' in info and info['regularMarketPrice']:
332
- return float(info['regularMarketPrice'])
333
-
334
- # Fallback to history
335
- hist = ticker.history(period="1d")
336
- if not hist.empty:
337
- return float(hist['Close'].iloc[-1])
338
-
339
- return None
214
+ except Exception as e:
215
+ logger.error(f"Failed to get currency rates: {e}")
216
+ return {}
217
+
218
+ def get_fiat_rate(self, base: str, quote: str) -> Optional[float]:
219
+ """
220
+ Get exchange rate for fiat currency pair.
221
+
222
+ Args:
223
+ base: Base currency code
224
+ quote: Quote currency code
340
225
 
226
+ Returns:
227
+ Exchange rate or None if failed
228
+ """
229
+ # Handle same currency
230
+ if base.upper() == quote.upper():
231
+ return 1.0
232
+
233
+ self.yahoo_limiter()
234
+
235
+ try:
236
+ rate_obj = self.yahoo.fetch_rate(base, quote)
237
+ return rate_obj.rate
341
238
  except Exception as e:
342
- logger.debug(f"Failed to get fiat rate for {base_currency}/{quote_currency}: {e}")
239
+ logger.debug(f"Failed to get fiat rate {base}/{quote}: {e}")
343
240
  return None
344
241
 
345
242
  def build_currency_database_data(self) -> List[CurrencyRateInfo]:
346
243
  """
347
- Build complete currency data for database insertion.
244
+ Build complete currency database data combining fiat and crypto.
348
245
 
349
246
  Returns:
350
- List of currency rate information ready for database
247
+ List of currency rate info objects
351
248
  """
352
- logger.info("Building complete currency database data...")
249
+ currencies = []
353
250
 
354
- all_currencies = []
355
-
356
- # 1. Get fiat currencies
357
- logger.info("Processing fiat currencies...")
251
+ # Get fiat currencies
252
+ logger.info("Loading fiat currencies...")
358
253
  fiat_currencies = self.get_fiat_currency_list()
359
254
 
360
255
  for fiat in fiat_currencies:
361
- # Get USD rate (skip USD itself)
362
- if fiat.code == "USD":
256
+ # Get USD rate
257
+ if fiat.code == 'USD':
363
258
  usd_rate = 1.0
364
259
  else:
365
- usd_rate = self.get_fiat_rate(fiat.code, "USD")
260
+ usd_rate = self.get_fiat_rate(fiat.code, 'USD')
366
261
  if usd_rate is None:
367
- logger.warning(f"Could not get USD rate for {fiat.code}, skipping")
262
+ logger.warning(f"Could not get USD rate for {fiat.code}")
368
263
  continue
369
264
 
370
265
  currency_info = CurrencyRateInfo(
371
266
  code=fiat.code,
372
267
  name=fiat.name,
373
- symbol=fiat.symbol,
268
+ symbol=fiat.symbol or "$" if fiat.code == "USD" else "",
374
269
  currency_type="fiat",
375
270
  decimal_places=2,
376
271
  usd_rate=usd_rate,
377
- min_payment_amount=1.0,
378
- is_active=True
272
+ min_payment_amount=1.0
379
273
  )
380
- all_currencies.append(currency_info)
381
-
382
- logger.debug(f"Added fiat currency: {fiat.code} = {usd_rate} USD")
274
+ currencies.append(currency_info)
383
275
 
384
- # 2. Get cryptocurrencies
385
- logger.info("Processing cryptocurrencies...")
276
+ # Get cryptocurrencies
277
+ logger.info("Loading cryptocurrencies...")
386
278
  crypto_currencies = self.get_cryptocurrency_list()
387
279
 
388
- # Get rates in batches
389
- crypto_ids = [crypto.id for crypto in crypto_currencies]
390
- crypto_rates = self.get_currency_rates(crypto_ids, "usd")
391
-
392
- for crypto in crypto_currencies:
393
- if crypto.id not in crypto_rates:
394
- logger.warning(f"No USD rate found for {crypto.symbol}, skipping")
395
- continue
280
+ if crypto_currencies:
281
+ # Get rates for all cryptos
282
+ crypto_symbols = [crypto.symbol for crypto in crypto_currencies]
283
+ rates = self.get_currency_rates(crypto_symbols, 'usd')
396
284
 
397
- usd_rate = crypto_rates[crypto.id]
398
-
399
- # Determine decimal places based on price
400
- if usd_rate >= 1:
401
- decimal_places = 2
402
- elif usd_rate >= 0.01:
403
- decimal_places = 4
404
- else:
405
- decimal_places = 8
406
-
407
- # Determine minimum payment amount
408
- if usd_rate >= 100:
409
- min_payment = 0.001
410
- elif usd_rate >= 1:
411
- min_payment = 0.01
412
- else:
413
- min_payment = 1.0
414
-
415
- currency_info = CurrencyRateInfo(
416
- code=crypto.symbol,
417
- name=crypto.name,
418
- symbol=crypto.symbol, # Use symbol as symbol for crypto
419
- currency_type="crypto",
420
- decimal_places=decimal_places,
421
- usd_rate=usd_rate,
422
- min_payment_amount=min_payment,
423
- is_active=True
424
- )
425
- all_currencies.append(currency_info)
426
-
427
- logger.debug(f"Added cryptocurrency: {crypto.symbol} = {usd_rate} USD")
285
+ for crypto in crypto_currencies:
286
+ if crypto.symbol in rates:
287
+ usd_rate = rates[crypto.symbol]
288
+
289
+ # Calculate appropriate decimal places based on USD value
290
+ if usd_rate >= 1:
291
+ decimal_places = 2
292
+ elif usd_rate >= 0.01:
293
+ decimal_places = 4
294
+ else:
295
+ decimal_places = 8
296
+
297
+ # Calculate minimum payment amount (roughly $1 worth)
298
+ min_payment = max(1.0 / usd_rate, 0.000001)
299
+
300
+ currency_info = CurrencyRateInfo(
301
+ code=crypto.symbol,
302
+ name=crypto.name,
303
+ symbol="", # We don't have crypto symbols
304
+ currency_type="crypto",
305
+ decimal_places=decimal_places,
306
+ usd_rate=usd_rate,
307
+ min_payment_amount=min_payment
308
+ )
309
+ currencies.append(currency_info)
428
310
 
429
- logger.info(f"Built currency data for {len(all_currencies)} currencies "
430
- f"({len(fiat_currencies)} fiat, {len(crypto_currencies)} crypto)")
431
-
432
- return all_currencies
311
+ logger.info(f"Built database data for {len(currencies)} currencies")
312
+ return currencies
433
313
 
434
314
  def get_statistics(self) -> Dict[str, int]:
435
- """Get statistics about available currencies."""
315
+ """Get loader statistics."""
436
316
  fiat_count = len(self.get_fiat_currency_list())
437
317
  crypto_count = len(self.get_cryptocurrency_list())
438
318
 
439
319
  return {
440
- "total_fiat_currencies": fiat_count,
441
- "total_cryptocurrencies": crypto_count,
442
- "total_currencies": fiat_count + crypto_count,
443
- "max_cryptocurrencies": self.config.max_cryptocurrencies,
444
- "max_fiat_currencies": self.config.max_fiat_currencies,
445
- "min_market_cap_usd": int(self.config.min_market_cap_usd)
320
+ 'total_fiat_currencies': fiat_count,
321
+ 'total_cryptocurrencies': crypto_count,
322
+ 'total_currencies': fiat_count + crypto_count,
323
+ 'max_cryptocurrencies': self.config.max_cryptocurrencies,
324
+ 'max_fiat_currencies': self.config.max_fiat_currencies,
325
+ 'min_market_cap_usd': self.config.min_market_cap_usd
446
326
  }
447
327
 
448
328
 
@@ -454,7 +334,8 @@ def create_database_loader(
454
334
  max_cryptocurrencies: int = 500,
455
335
  max_fiat_currencies: int = 50,
456
336
  min_market_cap_usd: float = 1000000,
457
- coingecko_delay: float = 1.5
337
+ yahoo_delay: float = 1.0,
338
+ coinpaprika_delay: float = 0.5
458
339
  ) -> CurrencyDatabaseLoader:
459
340
  """
460
341
  Create a configured database loader.
@@ -463,7 +344,8 @@ def create_database_loader(
463
344
  max_cryptocurrencies: Maximum number of cryptocurrencies to load
464
345
  max_fiat_currencies: Maximum number of fiat currencies to load
465
346
  min_market_cap_usd: Minimum market cap for cryptocurrencies
466
- coingecko_delay: Delay between CoinGecko requests
347
+ yahoo_delay: Delay between Yahoo Finance requests
348
+ coinpaprika_delay: Delay between CoinPaprika requests
467
349
 
468
350
  Returns:
469
351
  Configured CurrencyDatabaseLoader instance
@@ -472,24 +354,24 @@ def create_database_loader(
472
354
  max_cryptocurrencies=max_cryptocurrencies,
473
355
  max_fiat_currencies=max_fiat_currencies,
474
356
  min_market_cap_usd=min_market_cap_usd,
475
- coingecko_delay=coingecko_delay
357
+ yahoo_delay=yahoo_delay,
358
+ coinpaprika_delay=coinpaprika_delay
476
359
  )
477
-
478
360
  return CurrencyDatabaseLoader(config)
479
361
 
480
362
 
481
363
  def load_currencies_to_database_format() -> List[Dict]:
482
364
  """
483
- Convenience function to get currency data in Django ORM format.
365
+ Load currencies and convert to database format.
484
366
 
485
367
  Returns:
486
- List of dictionaries ready for bulk_create
368
+ List of currency dictionaries ready for database insertion
487
369
  """
488
370
  loader = create_database_loader()
489
371
  currencies = loader.build_currency_database_data()
490
372
 
491
- # Convert to dictionary format for Django ORM
492
- currency_dicts = []
373
+ # Convert to dict format
374
+ result = []
493
375
  for currency in currencies:
494
376
  currency_dict = {
495
377
  'code': currency.code,
@@ -499,9 +381,8 @@ def load_currencies_to_database_format() -> List[Dict]:
499
381
  'decimal_places': currency.decimal_places,
500
382
  'usd_rate': currency.usd_rate,
501
383
  'min_payment_amount': currency.min_payment_amount,
502
- 'is_active': currency.is_active,
503
384
  'rate_updated_at': datetime.now()
504
385
  }
505
- currency_dicts.append(currency_dict)
386
+ result.append(currency_dict)
506
387
 
507
- return currency_dicts
388
+ return result