django-cfg 1.3.5__py3-none-any.whl → 1.3.9__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 (252) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +24 -8
  3. django_cfg/apps/accounts/admin/activity_admin.py +146 -0
  4. django_cfg/apps/accounts/admin/filters.py +98 -22
  5. django_cfg/apps/accounts/admin/group_admin.py +86 -0
  6. django_cfg/apps/accounts/admin/inlines.py +42 -13
  7. django_cfg/apps/accounts/admin/otp_admin.py +115 -0
  8. django_cfg/apps/accounts/admin/registration_admin.py +173 -0
  9. django_cfg/apps/accounts/admin/resources.py +123 -19
  10. django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
  11. django_cfg/apps/accounts/admin/user_admin.py +362 -0
  12. django_cfg/apps/agents/admin/__init__.py +17 -4
  13. django_cfg/apps/agents/admin/execution_admin.py +204 -183
  14. django_cfg/apps/agents/admin/registry_admin.py +230 -255
  15. django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
  16. django_cfg/apps/agents/core/__init__.py +1 -1
  17. django_cfg/apps/agents/core/django_agent.py +221 -0
  18. django_cfg/apps/agents/core/exceptions.py +14 -0
  19. django_cfg/apps/agents/core/orchestrator.py +18 -3
  20. django_cfg/apps/knowbase/admin/__init__.py +1 -1
  21. django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
  22. django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
  23. django_cfg/apps/knowbase/admin/document_admin.py +269 -262
  24. django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
  25. django_cfg/apps/knowbase/config/settings.py +21 -4
  26. django_cfg/apps/knowbase/views/chat_views.py +3 -0
  27. django_cfg/apps/leads/admin/__init__.py +3 -1
  28. django_cfg/apps/leads/admin/leads_admin.py +235 -35
  29. django_cfg/apps/maintenance/admin/__init__.py +2 -2
  30. django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
  31. django_cfg/apps/maintenance/admin/log_admin.py +143 -61
  32. django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
  33. django_cfg/apps/maintenance/admin/site_admin.py +213 -352
  34. django_cfg/apps/newsletter/admin/__init__.py +29 -2
  35. django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
  36. django_cfg/apps/payments/admin/__init__.py +18 -27
  37. django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
  38. django_cfg/apps/payments/admin/balance_admin.py +166 -632
  39. django_cfg/apps/payments/admin/currencies_admin.py +235 -607
  40. django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
  41. django_cfg/apps/payments/admin/filters.py +83 -3
  42. django_cfg/apps/payments/admin/networks_admin.py +258 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +171 -461
  44. django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
  45. django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
  46. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
  48. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  49. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
  50. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  51. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  52. django_cfg/apps/payments/middleware/api_access.py +32 -6
  53. django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
  54. django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
  55. django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
  56. django_cfg/apps/payments/models/balance.py +12 -0
  57. django_cfg/apps/payments/models/currencies.py +106 -32
  58. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  59. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  60. django_cfg/apps/payments/services/core/payment_service.py +1 -1
  61. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  62. django_cfg/apps/payments/services/providers/base.py +95 -39
  63. django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
  64. django_cfg/apps/payments/services/providers/models/base.py +122 -0
  65. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  66. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  67. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  68. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  69. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  70. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  71. django_cfg/apps/payments/services/providers/{nowpayments.py → nowpayments/provider.py} +240 -209
  72. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  73. django_cfg/apps/payments/services/providers/registry.py +4 -32
  74. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  75. django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
  76. django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
  77. django_cfg/apps/payments/tasks/__init__.py +39 -0
  78. django_cfg/apps/payments/tasks/types.py +73 -0
  79. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  80. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  81. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  82. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  83. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  84. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  85. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  86. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  87. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  88. django_cfg/apps/payments/urls_admin.py +1 -1
  89. django_cfg/apps/payments/views/api/currencies.py +5 -5
  90. django_cfg/apps/payments/views/overview/services.py +2 -2
  91. django_cfg/apps/payments/views/serializers/currencies.py +4 -3
  92. django_cfg/apps/support/admin/__init__.py +10 -1
  93. django_cfg/apps/support/admin/support_admin.py +338 -141
  94. django_cfg/apps/tasks/admin/__init__.py +11 -0
  95. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  96. django_cfg/apps/urls.py +1 -2
  97. django_cfg/config.py +1 -1
  98. django_cfg/core/config.py +10 -5
  99. django_cfg/core/generation.py +1 -1
  100. django_cfg/management/commands/__init__.py +13 -1
  101. django_cfg/management/commands/app_agent_diagnose.py +470 -0
  102. django_cfg/management/commands/app_agent_generate.py +342 -0
  103. django_cfg/management/commands/app_agent_info.py +308 -0
  104. django_cfg/management/commands/migrate_all.py +9 -3
  105. django_cfg/management/commands/migrator.py +11 -6
  106. django_cfg/management/commands/rundramatiq.py +3 -2
  107. django_cfg/middleware/__init__.py +0 -2
  108. django_cfg/models/api_keys.py +115 -0
  109. django_cfg/modules/django_admin/__init__.py +64 -0
  110. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  111. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  112. django_cfg/modules/django_admin/decorators/display.py +106 -0
  113. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  114. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  115. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  116. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  117. django_cfg/modules/django_admin/models/__init__.py +20 -0
  118. django_cfg/modules/django_admin/models/action_models.py +33 -0
  119. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  120. django_cfg/modules/django_admin/models/base.py +26 -0
  121. django_cfg/modules/django_admin/models/display_models.py +31 -0
  122. django_cfg/modules/django_admin/utils/badges.py +159 -0
  123. django_cfg/modules/django_admin/utils/displays.py +247 -0
  124. django_cfg/modules/django_app_agent/__init__.py +87 -0
  125. django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
  126. django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
  127. django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
  128. django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
  129. django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
  130. django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
  131. django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
  132. django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
  133. django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
  134. django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
  135. django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
  136. django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
  137. django_cfg/modules/django_app_agent/core/__init__.py +33 -0
  138. django_cfg/modules/django_app_agent/core/config.py +300 -0
  139. django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
  140. django_cfg/modules/django_app_agent/models/__init__.py +71 -0
  141. django_cfg/modules/django_app_agent/models/base.py +283 -0
  142. django_cfg/modules/django_app_agent/models/context.py +496 -0
  143. django_cfg/modules/django_app_agent/models/enums.py +481 -0
  144. django_cfg/modules/django_app_agent/models/requests.py +500 -0
  145. django_cfg/modules/django_app_agent/models/responses.py +585 -0
  146. django_cfg/modules/django_app_agent/pytest.ini +6 -0
  147. django_cfg/modules/django_app_agent/services/__init__.py +42 -0
  148. django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
  149. django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
  150. django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
  151. django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
  152. django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
  153. django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
  154. django_cfg/modules/django_app_agent/services/base.py +437 -0
  155. django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
  156. django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
  157. django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
  158. django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
  159. django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
  160. django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
  161. django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
  162. django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
  163. django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
  164. django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
  165. django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
  166. django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
  167. django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
  168. django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
  169. django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
  170. django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
  171. django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
  172. django_cfg/modules/django_app_agent/services/report_service.py +332 -0
  173. django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
  174. django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
  175. django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
  176. django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
  177. django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
  178. django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
  179. django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
  180. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
  181. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
  182. django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
  183. django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
  184. django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
  185. django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
  186. django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
  187. django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
  188. django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
  189. django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
  190. django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
  191. django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
  192. django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
  193. django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
  194. django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
  195. django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
  196. django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
  197. django_cfg/modules/django_app_agent/ui/cli.py +419 -0
  198. django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
  199. django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
  200. django_cfg/modules/django_app_agent/utils/logging.py +360 -0
  201. django_cfg/modules/django_app_agent/utils/validation.py +417 -0
  202. django_cfg/modules/django_currency/__init__.py +2 -2
  203. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  204. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  205. django_cfg/modules/django_currency/core/converter.py +12 -12
  206. django_cfg/modules/django_currency/database/__init__.py +2 -2
  207. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  208. django_cfg/modules/django_llm/llm/client.py +10 -2
  209. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  210. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  211. django_cfg/modules/django_unfold/dashboard.py +14 -13
  212. django_cfg/modules/django_unfold/models/config.py +1 -1
  213. django_cfg/registry/core.py +3 -0
  214. django_cfg/registry/third_party.py +2 -2
  215. django_cfg/template_archive/django_sample.zip +0 -0
  216. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
  217. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/RECORD +224 -118
  218. django_cfg/apps/accounts/admin/activity.py +0 -96
  219. django_cfg/apps/accounts/admin/group.py +0 -17
  220. django_cfg/apps/accounts/admin/otp.py +0 -59
  221. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  222. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  223. django_cfg/apps/accounts/admin/user.py +0 -300
  224. django_cfg/apps/agents/core/agent.py +0 -281
  225. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  226. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  227. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  228. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  229. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  230. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  231. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  232. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  233. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  234. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  235. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  236. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  237. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  238. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  239. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  240. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  241. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  242. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  243. django_cfg/apps/tasks/admin.py +0 -320
  244. django_cfg/middleware/static_nocache.py +0 -55
  245. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  246. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  247. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  248. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  249. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  250. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
  251. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
  252. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,587 @@
1
+ """
2
+ Hybrid Currency Client - Multi-source currency rate fetcher with full caching
3
+ Combines multiple data sources for maximum reliability and performance:
4
+ 1. Fawaz Currency API (primary - 200+ currencies via CDN)
5
+ 2. Frankfurter API (reliable fiat currencies)
6
+ 3. ExchangeRate-API (fallback)
7
+ 4. CBR API (for RUB rates)
8
+
9
+ All supported currencies are dynamically fetched and cached.
10
+ """
11
+
12
+ import logging
13
+ import requests
14
+ import time
15
+ import random
16
+ from datetime import datetime, timedelta
17
+ from typing import Dict, Set, Optional, List, Tuple
18
+ from cachetools import TTLCache
19
+
20
+ from ..core.models import Rate
21
+ from ..core.exceptions import RateFetchError
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class HybridCurrencyClient:
27
+ """Multi-source currency client with intelligent fallback and full caching."""
28
+
29
+ def __init__(self, cache_ttl: int = 3600):
30
+ """Initialize hybrid client with multiple data sources."""
31
+ self._rate_cache = TTLCache(maxsize=1000, ttl=cache_ttl)
32
+ self._session = requests.Session()
33
+
34
+ # User-Agent rotation for better reliability
35
+ self._user_agents = [
36
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
37
+ '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',
38
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
39
+ 'curl/7.68.0',
40
+ 'python-requests/2.31.0'
41
+ ]
42
+
43
+ # Data sources configuration (ordered by priority)
44
+ self._sources = {
45
+ 'fawaz_currency': {
46
+ 'url': 'https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies',
47
+ 'priority': 1,
48
+ 'rate_limit': 0.5,
49
+ 'supports_check': self._fawaz_supports_pair,
50
+ 'fetch_method': self._fetch_from_fawaz_currency,
51
+ 'get_supported': self._get_fawaz_supported_currencies
52
+ },
53
+ 'prebid_currency': {
54
+ 'url': 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json',
55
+ 'priority': 2,
56
+ 'rate_limit': 0.5,
57
+ 'supports_check': self._prebid_supports_pair,
58
+ 'fetch_method': self._fetch_from_prebid_currency,
59
+ 'get_supported': self._get_prebid_supported_currencies
60
+ },
61
+ 'frankfurter': {
62
+ 'url': 'https://api.frankfurter.app/latest',
63
+ 'priority': 3,
64
+ 'rate_limit': 1.0,
65
+ 'supports_check': self._frankfurter_supports_pair,
66
+ 'fetch_method': self._fetch_from_frankfurter,
67
+ 'get_supported': self._get_frankfurter_supported_currencies
68
+ },
69
+ 'exchangerate_api': {
70
+ 'url': 'https://open.er-api.com/v6/latest',
71
+ 'priority': 4,
72
+ 'rate_limit': 1.5,
73
+ 'supports_check': self._exchangerate_supports_pair,
74
+ 'fetch_method': self._fetch_from_exchangerate_api,
75
+ 'get_supported': self._get_exchangerate_supported_currencies
76
+ },
77
+ 'cbr': {
78
+ 'url': 'https://www.cbr-xml-daily.ru/daily_json.js',
79
+ 'priority': 5,
80
+ 'rate_limit': 1.0,
81
+ 'supports_check': self._cbr_supports_pair,
82
+ 'fetch_method': self._fetch_from_cbr,
83
+ 'get_supported': self._get_cbr_supported_currencies
84
+ }
85
+ }
86
+
87
+ self._last_request_times = {}
88
+ self._max_retries = 2
89
+
90
+ def _get_random_user_agent(self) -> str:
91
+ """Get random User-Agent for better request reliability."""
92
+ return random.choice(self._user_agents)
93
+
94
+ def _make_request_with_retry(self, url: str, source: str, headers: dict = None) -> requests.Response:
95
+ """Make HTTP request with exponential backoff retry logic."""
96
+ source_config = self._sources[source]
97
+ rate_limit = source_config['rate_limit']
98
+
99
+ # Rate limiting
100
+ last_request = self._last_request_times.get(source, 0)
101
+ time_since_last = time.time() - last_request
102
+ if time_since_last < rate_limit:
103
+ sleep_time = rate_limit - time_since_last + random.uniform(0, 0.5)
104
+ time.sleep(sleep_time)
105
+
106
+ for attempt in range(self._max_retries + 1):
107
+ try:
108
+ request_headers = {
109
+ 'User-Agent': self._get_random_user_agent(),
110
+ 'Accept': 'application/json',
111
+ 'Accept-Language': 'en-US,en;q=0.9'
112
+ }
113
+ if headers:
114
+ request_headers.update(headers)
115
+
116
+ response = self._session.get(url, headers=request_headers, timeout=10)
117
+ self._last_request_times[source] = time.time()
118
+
119
+ if response.status_code == 429:
120
+ if attempt < self._max_retries:
121
+ backoff = (2 ** attempt) * 3 + random.uniform(1, 2)
122
+ logger.warning(f"{source}: Rate limited, retrying in {backoff:.1f}s")
123
+ time.sleep(backoff)
124
+ continue
125
+ else:
126
+ raise requests.exceptions.HTTPError(f"429 Too Many Requests from {source}")
127
+
128
+ response.raise_for_status()
129
+ return response
130
+
131
+ except requests.exceptions.RequestException as e:
132
+ if attempt < self._max_retries:
133
+ backoff = (2 ** attempt) * 2 + random.uniform(0.5, 1)
134
+ logger.warning(f"{source}: Request failed, retrying in {backoff:.1f}s - {e}")
135
+ time.sleep(backoff)
136
+ continue
137
+ else:
138
+ raise RateFetchError(f"{source} request failed: {e}")
139
+
140
+ raise RateFetchError(f"{source}: Failed after {self._max_retries + 1} attempts")
141
+
142
+ # ============================================================================
143
+ # FAWAZ CURRENCY API
144
+ # ============================================================================
145
+
146
+ def _get_fawaz_supported_currencies(self) -> Set[str]:
147
+ """Get list of supported currencies from Fawaz API with caching."""
148
+ cache_key = "fawaz_supported_currencies"
149
+
150
+ if cache_key in self._rate_cache:
151
+ return self._rate_cache[cache_key]
152
+
153
+ try:
154
+ url = f"{self._sources['fawaz_currency']['url']}/usd.json"
155
+ response = self._make_request_with_retry(url, 'fawaz_currency')
156
+ data = response.json()
157
+
158
+ if 'usd' in data:
159
+ supported = set(data['usd'].keys())
160
+ supported.add('usd') # Add USD itself
161
+ logger.info(f"Fawaz API supports {len(supported)} currencies")
162
+
163
+ self._rate_cache[cache_key] = supported
164
+ return supported
165
+ else:
166
+ logger.warning("Fawaz API response format unexpected")
167
+ return set()
168
+
169
+ except Exception as e:
170
+ logger.warning(f"Failed to get Fawaz supported currencies: {e}")
171
+ # Minimal fallback
172
+ fallback = {'usd', 'eur', 'btc', 'eth'}
173
+ self._rate_cache[cache_key] = fallback
174
+ return fallback
175
+
176
+ def _fawaz_supports_pair(self, base: str, quote: str) -> bool:
177
+ """Check if Fawaz Currency API supports the currency pair."""
178
+ supported_currencies = self._get_fawaz_supported_currencies()
179
+ base_lower = base.lower()
180
+ quote_lower = quote.lower()
181
+ return base_lower in supported_currencies and quote_lower in supported_currencies
182
+
183
+ def _fetch_from_fawaz_currency(self, base: str, quote: str) -> Rate:
184
+ """Fetch rate from Fawaz Currency API via jsDelivr CDN."""
185
+ base_lower = base.lower()
186
+ url = f"{self._sources['fawaz_currency']['url']}/{base_lower}.json"
187
+ response = self._make_request_with_retry(url, 'fawaz_currency')
188
+ data = response.json()
189
+
190
+ if base_lower not in data:
191
+ raise RateFetchError(f"Fawaz API doesn't have base currency {base}")
192
+
193
+ rates = data[base_lower]
194
+ quote_lower = quote.lower()
195
+
196
+ if quote_lower not in rates:
197
+ raise RateFetchError(f"Fawaz API doesn't have {base}/{quote} rate")
198
+
199
+ return Rate(
200
+ source="fawaz_currency",
201
+ base_currency=base.upper(),
202
+ quote_currency=quote.upper(),
203
+ rate=float(rates[quote_lower]),
204
+ timestamp=datetime.now()
205
+ )
206
+
207
+ # ============================================================================
208
+ # PREBID CURRENCY API
209
+ # ============================================================================
210
+
211
+ def _get_prebid_supported_currencies(self) -> Set[str]:
212
+ """Get list of supported currencies from Prebid Currency API with caching."""
213
+ cache_key = "prebid_supported_currencies"
214
+
215
+ if cache_key in self._rate_cache:
216
+ return self._rate_cache[cache_key]
217
+
218
+ try:
219
+ url = self._sources['prebid_currency']['url']
220
+ response = self._make_request_with_retry(url, 'prebid_currency')
221
+ data = response.json()
222
+
223
+ if 'conversions' in data:
224
+ supported = set()
225
+ # Prebid has conversions from multiple base currencies
226
+ for base_currency, rates in data['conversions'].items():
227
+ supported.add(base_currency.upper())
228
+ supported.update(rate.upper() for rate in rates.keys())
229
+
230
+ logger.info(f"Prebid Currency API supports {len(supported)} currencies")
231
+
232
+ self._rate_cache[cache_key] = supported
233
+ return supported
234
+ else:
235
+ logger.warning("Prebid Currency API response format unexpected")
236
+ return set()
237
+
238
+ except Exception as e:
239
+ logger.warning(f"Failed to get Prebid supported currencies: {e}")
240
+ # Fallback to major currencies
241
+ fallback = {'USD', 'EUR', 'GBP', 'JPY', 'CHF', 'CAD', 'AUD'}
242
+ self._rate_cache[cache_key] = fallback
243
+ return fallback
244
+
245
+ def _prebid_supports_pair(self, base: str, quote: str) -> bool:
246
+ """Check if Prebid Currency API supports the currency pair."""
247
+ supported_currencies = self._get_prebid_supported_currencies()
248
+ return base.upper() in supported_currencies and quote.upper() in supported_currencies
249
+
250
+ def _fetch_from_prebid_currency(self, base: str, quote: str) -> Rate:
251
+ """Fetch rate from Prebid Currency API."""
252
+ url = self._sources['prebid_currency']['url']
253
+ response = self._make_request_with_retry(url, 'prebid_currency')
254
+ data = response.json()
255
+
256
+ base_upper = base.upper()
257
+ quote_upper = quote.upper()
258
+
259
+ # Check if we have direct conversion from base to quote
260
+ if 'conversions' in data and base_upper in data['conversions']:
261
+ rates = data['conversions'][base_upper]
262
+ if quote_upper in rates:
263
+ return Rate(
264
+ source="prebid_currency",
265
+ base_currency=base_upper,
266
+ quote_currency=quote_upper,
267
+ rate=float(rates[quote_upper]),
268
+ timestamp=datetime.now()
269
+ )
270
+
271
+ # Try reverse conversion (quote to base)
272
+ if 'conversions' in data and quote_upper in data['conversions']:
273
+ rates = data['conversions'][quote_upper]
274
+ if base_upper in rates:
275
+ reverse_rate = float(rates[base_upper])
276
+ if reverse_rate > 0:
277
+ return Rate(
278
+ source="prebid_currency",
279
+ base_currency=base_upper,
280
+ quote_currency=quote_upper,
281
+ rate=1.0 / reverse_rate,
282
+ timestamp=datetime.now()
283
+ )
284
+
285
+ raise RateFetchError(f"Prebid Currency API doesn't have {base}/{quote} rate")
286
+
287
+ # ============================================================================
288
+ # FRANKFURTER API
289
+ # ============================================================================
290
+
291
+ def _get_frankfurter_supported_currencies(self) -> Set[str]:
292
+ """Get list of supported currencies from Frankfurter API with caching."""
293
+ cache_key = "frankfurter_supported_currencies"
294
+
295
+ if cache_key in self._rate_cache:
296
+ return self._rate_cache[cache_key]
297
+
298
+ try:
299
+ url = f"{self._sources['frankfurter']['url']}"
300
+ response = self._make_request_with_retry(url, 'frankfurter')
301
+ data = response.json()
302
+
303
+ if 'rates' in data:
304
+ supported = set(data['rates'].keys())
305
+ supported.add('EUR') # Add EUR itself (base currency)
306
+ logger.info(f"Frankfurter API supports {len(supported)} currencies")
307
+
308
+ self._rate_cache[cache_key] = supported
309
+ return supported
310
+ else:
311
+ logger.warning("Frankfurter API response format unexpected")
312
+ return set()
313
+
314
+ except Exception as e:
315
+ logger.warning(f"Failed to get Frankfurter supported currencies: {e}")
316
+ # Fallback to major fiat currencies
317
+ fallback = {'EUR', 'USD', 'GBP', 'JPY', 'CHF', 'CAD', 'AUD'}
318
+ self._rate_cache[cache_key] = fallback
319
+ return fallback
320
+
321
+ def _frankfurter_supports_pair(self, base: str, quote: str) -> bool:
322
+ """Check if Frankfurter supports the currency pair."""
323
+ supported_currencies = self._get_frankfurter_supported_currencies()
324
+ return base.upper() in supported_currencies and quote.upper() in supported_currencies
325
+
326
+ def _fetch_from_frankfurter(self, base: str, quote: str) -> Rate:
327
+ """Fetch rate from Frankfurter API."""
328
+ url = f"{self._sources['frankfurter']['url']}?from={base}&to={quote}"
329
+ response = self._make_request_with_retry(url, 'frankfurter')
330
+ data = response.json()
331
+
332
+ if 'rates' not in data or quote.upper() not in data['rates']:
333
+ raise RateFetchError(f"Frankfurter doesn't have {base}/{quote} rate")
334
+
335
+ return Rate(
336
+ source="frankfurter",
337
+ base_currency=base.upper(),
338
+ quote_currency=quote.upper(),
339
+ rate=float(data['rates'][quote.upper()]),
340
+ timestamp=datetime.now()
341
+ )
342
+
343
+ # ============================================================================
344
+ # EXCHANGERATE-API
345
+ # ============================================================================
346
+
347
+ def _get_exchangerate_supported_currencies(self) -> Set[str]:
348
+ """Get list of supported currencies from ExchangeRate-API with caching."""
349
+ cache_key = "exchangerate_supported_currencies"
350
+
351
+ if cache_key in self._rate_cache:
352
+ return self._rate_cache[cache_key]
353
+
354
+ try:
355
+ url = f"{self._sources['exchangerate_api']['url']}/USD"
356
+ response = self._make_request_with_retry(url, 'exchangerate_api')
357
+ data = response.json()
358
+
359
+ if data.get('result') == 'success' and 'rates' in data:
360
+ supported = set(data['rates'].keys())
361
+ supported.add('USD') # Add USD itself (base currency)
362
+ logger.info(f"ExchangeRate-API supports {len(supported)} currencies")
363
+
364
+ self._rate_cache[cache_key] = supported
365
+ return supported
366
+ else:
367
+ logger.warning("ExchangeRate-API response format unexpected")
368
+ return set()
369
+
370
+ except Exception as e:
371
+ logger.warning(f"Failed to get ExchangeRate-API supported currencies: {e}")
372
+ # Fallback to major currencies
373
+ fallback = {'USD', 'EUR', 'GBP', 'JPY', 'CHF', 'CAD', 'AUD', 'RUB'}
374
+ self._rate_cache[cache_key] = fallback
375
+ return fallback
376
+
377
+ def _exchangerate_supports_pair(self, base: str, quote: str) -> bool:
378
+ """Check if ExchangeRate-API supports the currency pair."""
379
+ supported_currencies = self._get_exchangerate_supported_currencies()
380
+ return base.upper() in supported_currencies and quote.upper() in supported_currencies
381
+
382
+ def _fetch_from_exchangerate_api(self, base: str, quote: str) -> Rate:
383
+ """Fetch rate from ExchangeRate-API."""
384
+ url = f"{self._sources['exchangerate_api']['url']}/{base.upper()}"
385
+ response = self._make_request_with_retry(url, 'exchangerate_api')
386
+ data = response.json()
387
+
388
+ if data.get('result') != 'success':
389
+ raise RateFetchError(f"ExchangeRate-API error: {data.get('error-type', 'Unknown error')}")
390
+
391
+ rates = data.get('rates', {})
392
+ if quote.upper() not in rates:
393
+ raise RateFetchError(f"ExchangeRate-API doesn't have {quote} rate")
394
+
395
+ return Rate(
396
+ source="exchangerate_api",
397
+ base_currency=base.upper(),
398
+ quote_currency=quote.upper(),
399
+ rate=float(rates[quote.upper()]),
400
+ timestamp=datetime.now()
401
+ )
402
+
403
+ # ============================================================================
404
+ # CBR API (Russian Central Bank)
405
+ # ============================================================================
406
+
407
+ def _get_cbr_supported_currencies(self) -> Set[str]:
408
+ """Get list of supported currencies from CBR API with caching."""
409
+ cache_key = "cbr_supported_currencies"
410
+
411
+ if cache_key in self._rate_cache:
412
+ return self._rate_cache[cache_key]
413
+
414
+ try:
415
+ url = self._sources['cbr']['url']
416
+ response = self._make_request_with_retry(url, 'cbr')
417
+ data = response.json()
418
+
419
+ if 'Valute' in data:
420
+ supported = set(data['Valute'].keys())
421
+ supported.add('RUB') # Add RUB itself
422
+ logger.info(f"CBR API supports {len(supported)} currencies")
423
+
424
+ self._rate_cache[cache_key] = supported
425
+ return supported
426
+ else:
427
+ logger.warning("CBR API response format unexpected")
428
+ return set()
429
+
430
+ except Exception as e:
431
+ logger.warning(f"Failed to get CBR supported currencies: {e}")
432
+ # Fallback to major currencies that CBR typically supports
433
+ fallback = {'RUB', 'USD', 'EUR', 'GBP', 'JPY', 'CHF', 'CNY'}
434
+ self._rate_cache[cache_key] = fallback
435
+ return fallback
436
+
437
+ def _cbr_supports_pair(self, base: str, quote: str) -> bool:
438
+ """CBR supports conversions to/from RUB."""
439
+ if 'RUB' not in [base.upper(), quote.upper()]:
440
+ return False
441
+
442
+ supported_currencies = self._get_cbr_supported_currencies()
443
+ return base.upper() in supported_currencies and quote.upper() in supported_currencies
444
+
445
+ def _fetch_from_cbr(self, base: str, quote: str) -> Rate:
446
+ """Fetch rate from CBR API (Russian Central Bank)."""
447
+ url = self._sources['cbr']['url']
448
+ response = self._make_request_with_retry(url, 'cbr')
449
+ data = response.json()
450
+
451
+ base, quote = base.upper(), quote.upper()
452
+
453
+ if base == 'RUB' and quote in data.get('Valute', {}):
454
+ # RUB to other currency
455
+ currency_data = data['Valute'][quote]
456
+ rate_value = 1.0 / (currency_data['Value'] / currency_data['Nominal'])
457
+ elif quote == 'RUB' and base in data.get('Valute', {}):
458
+ # Other currency to RUB
459
+ currency_data = data['Valute'][base]
460
+ rate_value = currency_data['Value'] / currency_data['Nominal']
461
+ else:
462
+ raise RateFetchError(f"CBR doesn't support {base}/{quote}")
463
+
464
+ return Rate(
465
+ source="cbr",
466
+ base_currency=base,
467
+ quote_currency=quote,
468
+ rate=rate_value,
469
+ timestamp=datetime.now()
470
+ )
471
+
472
+ # ============================================================================
473
+ # MAIN API METHODS
474
+ # ============================================================================
475
+
476
+ def fetch_rate(self, base: str, quote: str) -> Rate:
477
+ """
478
+ Fetch exchange rate using hybrid approach with priority fallback.
479
+
480
+ Tries sources in priority order:
481
+ 1. Fawaz Currency API (200+ currencies, unlimited, CDN-fast)
482
+ 2. Prebid Currency API (major fiat currencies, CDN-fast)
483
+ 3. Frankfurter (reliable, free, no limits)
484
+ 4. ExchangeRate-API (good fallback)
485
+ 5. CBR (best for RUB pairs)
486
+ """
487
+ base, quote = base.upper(), quote.upper()
488
+ cache_key = f"{base}_{quote}"
489
+
490
+ # Check cache first
491
+ if cache_key in self._rate_cache:
492
+ logger.debug(f"Retrieved rate {base}/{quote} from cache")
493
+ return self._rate_cache[cache_key]
494
+
495
+ # Try sources in priority order
496
+ sources_to_try = []
497
+ for source_name, config in self._sources.items():
498
+ if config['supports_check'](base, quote):
499
+ sources_to_try.append((config['priority'], source_name))
500
+
501
+ sources_to_try.sort(key=lambda x: x[0]) # Sort by priority
502
+
503
+ last_error = None
504
+ for priority, source_name in sources_to_try:
505
+ try:
506
+ logger.debug(f"Trying {source_name} for {base}/{quote}")
507
+
508
+ config = self._sources[source_name]
509
+ rate = config['fetch_method'](base, quote)
510
+
511
+ # Cache successful result
512
+ self._rate_cache[cache_key] = rate
513
+ logger.info(f"Fetched {base}/{quote} = {rate.rate} from {source_name}")
514
+ return rate
515
+
516
+ except Exception as e:
517
+ logger.warning(f"{source_name} failed for {base}/{quote}: {e}")
518
+ last_error = e
519
+ continue
520
+
521
+ raise RateFetchError(f"All sources failed for {base}/{quote}. Last error: {last_error}")
522
+
523
+ def supports_pair(self, base: str, quote: str) -> bool:
524
+ """Check if any source supports the currency pair."""
525
+ base, quote = base.upper(), quote.upper()
526
+ return any(
527
+ config['supports_check'](base, quote)
528
+ for config in self._sources.values()
529
+ )
530
+
531
+ def get_all_supported_currencies(self) -> Dict[str, str]:
532
+ """Get all supported currencies across all sources dynamically."""
533
+ cache_key = "all_supported_currencies"
534
+
535
+ # Check cache first
536
+ if cache_key in self._rate_cache:
537
+ return self._rate_cache[cache_key]
538
+
539
+ # Collect all currencies from all sources
540
+ all_currencies = set()
541
+ for source_name, config in self._sources.items():
542
+ try:
543
+ supported = config['get_supported']()
544
+ all_currencies.update(supported)
545
+ logger.debug(f"Added {len(supported)} currencies from {source_name}")
546
+ except Exception as e:
547
+ logger.warning(f"Failed to get supported currencies from {source_name}: {e}")
548
+
549
+ # Currency names mapping
550
+ currency_names = {
551
+ # Major Fiat
552
+ 'USD': 'US Dollar', 'EUR': 'Euro', 'GBP': 'British Pound',
553
+ 'JPY': 'Japanese Yen', 'CHF': 'Swiss Franc', 'CAD': 'Canadian Dollar',
554
+ 'AUD': 'Australian Dollar', 'NZD': 'New Zealand Dollar',
555
+ 'SEK': 'Swedish Krona', 'NOK': 'Norwegian Krone', 'DKK': 'Danish Krone',
556
+ 'PLN': 'Polish Zloty', 'CZK': 'Czech Koruna', 'HUF': 'Hungarian Forint',
557
+ 'RUB': 'Russian Ruble', 'CNY': 'Chinese Yuan', 'INR': 'Indian Rupee',
558
+ 'KRW': 'South Korean Won', 'SGD': 'Singapore Dollar', 'HKD': 'Hong Kong Dollar',
559
+ 'THB': 'Thai Baht', 'MXN': 'Mexican Peso', 'BRL': 'Brazilian Real',
560
+ 'ZAR': 'South African Rand', 'TRY': 'Turkish Lira', 'ILS': 'Israeli Shekel',
561
+
562
+ # Cryptocurrencies
563
+ 'BTC': 'Bitcoin', 'ETH': 'Ethereum', 'BNB': 'Binance Coin',
564
+ 'XRP': 'Ripple', 'ADA': 'Cardano', 'SOL': 'Solana',
565
+ 'DOT': 'Polkadot', 'MATIC': 'Polygon', 'LTC': 'Litecoin',
566
+ 'BCH': 'Bitcoin Cash', 'LINK': 'Chainlink', 'UNI': 'Uniswap',
567
+ 'ATOM': 'Cosmos', 'XLM': 'Stellar', 'VET': 'VeChain',
568
+ 'USDT': 'Tether USD', 'USDC': 'USD Coin', 'DAI': 'Dai Stablecoin',
569
+
570
+ # Precious Metals
571
+ 'XAU': 'Gold Ounce', 'XAG': 'Silver Ounce',
572
+ 'XPT': 'Platinum Ounce', 'XPD': 'Palladium Ounce'
573
+ }
574
+
575
+ # Create result with proper names
576
+ result = {}
577
+ for currency in sorted(all_currencies):
578
+ currency_upper = currency.upper()
579
+ # Test if currency actually works with USD (basic validation)
580
+ if self.supports_pair(currency_upper, 'USD'):
581
+ result[currency_upper] = currency_names.get(currency_upper, f"{currency_upper} Currency")
582
+
583
+ # Cache the result
584
+ self._rate_cache[cache_key] = result
585
+ logger.info(f"Collected {len(result)} supported currencies from all sources")
586
+
587
+ return result
@@ -7,7 +7,7 @@ from typing import Optional
7
7
 
8
8
  from .models import Rate, ConversionRequest, ConversionResult
9
9
  from .exceptions import ConversionError, CurrencyNotFoundError
10
- from ..clients import YahooFinanceClient, CoinPaprikaClient
10
+ from ..clients import HybridCurrencyClient, CoinPaprikaClient
11
11
  from ..utils.cache import CacheManager
12
12
 
13
13
  logger = logging.getLogger(__name__)
@@ -23,7 +23,7 @@ class CurrencyConverter:
23
23
  Args:
24
24
  cache_ttl: Cache TTL in seconds
25
25
  """
26
- self.yahoo = YahooFinanceClient(cache_ttl=cache_ttl)
26
+ self.hybrid = HybridCurrencyClient(cache_ttl=cache_ttl)
27
27
  self.coinpaprika = CoinPaprikaClient(cache_ttl=cache_ttl)
28
28
  self.cache = CacheManager(ttl=cache_ttl)
29
29
 
@@ -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 ["yahoo", "coinpaprika"]:
98
+ for source in ["hybrid", "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 CoinPaprika first (excellent for crypto, no rate limits)
104
- if self.coinpaprika.supports_pair(base, quote):
103
+ # Try Hybrid client first (multiple sources with fallback)
104
+ if self.hybrid.supports_pair(base, quote):
105
105
  try:
106
- rate = self.coinpaprika.fetch_rate(base, quote)
106
+ rate = self.hybrid.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"CoinPaprika failed for {base}/{quote}: {e}")
110
+ logger.warning(f"Hybrid client failed for {base}/{quote}: {e}")
111
111
 
112
- # Try Yahoo Finance next (good for fiat and major forex pairs)
113
- if self.yahoo.supports_pair(base, quote):
112
+ # Try CoinPaprika next (excellent for crypto, no rate limits)
113
+ if self.coinpaprika.supports_pair(base, quote):
114
114
  try:
115
- rate = self.yahoo.fetch_rate(base, quote)
115
+ rate = self.coinpaprika.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"Yahoo Finance failed for {base}/{quote}: {e}")
119
+ logger.warning(f"CoinPaprika failed for {base}/{quote}: {e}")
120
120
 
121
121
  # Try indirect conversion via USD
122
122
  if base != "USD" and quote != "USD":
@@ -159,6 +159,6 @@ class CurrencyConverter:
159
159
  def get_supported_currencies(self) -> dict:
160
160
  """Get list of supported currencies by provider."""
161
161
  return {
162
- "yahoo": self.yahoo.get_all_supported_currencies(),
162
+ "hybrid": self.hybrid.get_all_supported_currencies(),
163
163
  "coinpaprika": self.coinpaprika.get_all_supported_currencies()
164
164
  }
@@ -6,7 +6,7 @@ from .database_loader import (
6
6
  CurrencyDatabaseLoader,
7
7
  DatabaseLoaderConfig,
8
8
  CoinPaprikaCoinInfo,
9
- YahooFinanceCurrencyInfo,
9
+ HybridCurrencyInfo,
10
10
  CurrencyRateInfo,
11
11
  RateLimiter,
12
12
  create_database_loader,
@@ -17,7 +17,7 @@ __all__ = [
17
17
  'CurrencyDatabaseLoader',
18
18
  'DatabaseLoaderConfig',
19
19
  'CoinPaprikaCoinInfo',
20
- 'YahooFinanceCurrencyInfo',
20
+ 'HybridCurrencyInfo',
21
21
  'CurrencyRateInfo',
22
22
  'RateLimiter',
23
23
  'create_database_loader',