django-cfg 1.3.7__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/admin/__init__.py +24 -8
- django_cfg/apps/accounts/admin/activity_admin.py +146 -0
- django_cfg/apps/accounts/admin/filters.py +98 -22
- django_cfg/apps/accounts/admin/group_admin.py +86 -0
- django_cfg/apps/accounts/admin/inlines.py +42 -13
- django_cfg/apps/accounts/admin/otp_admin.py +115 -0
- django_cfg/apps/accounts/admin/registration_admin.py +173 -0
- django_cfg/apps/accounts/admin/resources.py +123 -19
- django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
- django_cfg/apps/accounts/admin/user_admin.py +362 -0
- django_cfg/apps/agents/admin/__init__.py +17 -4
- django_cfg/apps/agents/admin/execution_admin.py +204 -183
- django_cfg/apps/agents/admin/registry_admin.py +230 -255
- django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
- django_cfg/apps/agents/core/__init__.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +221 -0
- django_cfg/apps/agents/core/exceptions.py +14 -0
- django_cfg/apps/agents/core/orchestrator.py +18 -3
- django_cfg/apps/knowbase/admin/__init__.py +1 -1
- django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
- django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
- django_cfg/apps/knowbase/admin/document_admin.py +269 -262
- django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
- django_cfg/apps/knowbase/config/settings.py +21 -4
- django_cfg/apps/knowbase/views/chat_views.py +3 -0
- django_cfg/apps/leads/admin/__init__.py +3 -1
- django_cfg/apps/leads/admin/leads_admin.py +235 -35
- django_cfg/apps/maintenance/admin/__init__.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
- django_cfg/apps/maintenance/admin/log_admin.py +143 -61
- django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
- django_cfg/apps/maintenance/admin/site_admin.py +213 -352
- django_cfg/apps/newsletter/admin/__init__.py +29 -2
- django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
- django_cfg/apps/payments/admin/__init__.py +18 -27
- django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
- django_cfg/apps/payments/admin/balance_admin.py +166 -632
- django_cfg/apps/payments/admin/currencies_admin.py +235 -607
- django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
- django_cfg/apps/payments/admin/filters.py +83 -3
- django_cfg/apps/payments/admin/networks_admin.py +258 -0
- django_cfg/apps/payments/admin/payments_admin.py +171 -461
- django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
- django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
- django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
- django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
- django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
- django_cfg/apps/payments/middleware/api_access.py +32 -6
- django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
- django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
- django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
- django_cfg/apps/payments/models/balance.py +12 -0
- django_cfg/apps/payments/models/currencies.py +106 -32
- django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
- django_cfg/apps/payments/services/core/currency_service.py +35 -28
- django_cfg/apps/payments/services/core/payment_service.py +1 -1
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/base.py +95 -39
- django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
- django_cfg/apps/payments/services/providers/models/base.py +122 -0
- django_cfg/apps/payments/services/providers/models/providers.py +87 -0
- django_cfg/apps/payments/services/providers/models/universal.py +48 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
- django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
- django_cfg/apps/payments/services/providers/{nowpayments.py → nowpayments/provider.py} +240 -209
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
- django_cfg/apps/payments/services/providers/registry.py +4 -32
- django_cfg/apps/payments/services/providers/sync_service.py +277 -0
- django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
- django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
- django_cfg/apps/payments/tasks/__init__.py +39 -0
- django_cfg/apps/payments/tasks/types.py +73 -0
- django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
- django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
- django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
- django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
- django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
- django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
- django_cfg/apps/payments/urls_admin.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +5 -5
- django_cfg/apps/payments/views/overview/services.py +2 -2
- django_cfg/apps/payments/views/serializers/currencies.py +4 -3
- django_cfg/apps/support/admin/__init__.py +10 -1
- django_cfg/apps/support/admin/support_admin.py +338 -141
- django_cfg/apps/tasks/admin/__init__.py +11 -0
- django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +10 -5
- django_cfg/core/generation.py +1 -1
- django_cfg/management/commands/__init__.py +13 -1
- django_cfg/management/commands/app_agent_diagnose.py +470 -0
- django_cfg/management/commands/app_agent_generate.py +342 -0
- django_cfg/management/commands/app_agent_info.py +308 -0
- django_cfg/management/commands/migrate_all.py +9 -3
- django_cfg/management/commands/migrator.py +11 -6
- django_cfg/management/commands/rundramatiq.py +3 -2
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/models/api_keys.py +115 -0
- django_cfg/modules/django_admin/__init__.py +64 -0
- django_cfg/modules/django_admin/decorators/__init__.py +13 -0
- django_cfg/modules/django_admin/decorators/actions.py +106 -0
- django_cfg/modules/django_admin/decorators/display.py +106 -0
- django_cfg/modules/django_admin/mixins/__init__.py +14 -0
- django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
- django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
- django_cfg/modules/django_admin/models/__init__.py +20 -0
- django_cfg/modules/django_admin/models/action_models.py +33 -0
- django_cfg/modules/django_admin/models/badge_models.py +20 -0
- django_cfg/modules/django_admin/models/base.py +26 -0
- django_cfg/modules/django_admin/models/display_models.py +31 -0
- django_cfg/modules/django_admin/utils/badges.py +159 -0
- django_cfg/modules/django_admin/utils/displays.py +247 -0
- django_cfg/modules/django_app_agent/__init__.py +87 -0
- django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
- django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
- django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
- django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
- django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
- django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
- django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
- django_cfg/modules/django_app_agent/core/__init__.py +33 -0
- django_cfg/modules/django_app_agent/core/config.py +300 -0
- django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
- django_cfg/modules/django_app_agent/models/__init__.py +71 -0
- django_cfg/modules/django_app_agent/models/base.py +283 -0
- django_cfg/modules/django_app_agent/models/context.py +496 -0
- django_cfg/modules/django_app_agent/models/enums.py +481 -0
- django_cfg/modules/django_app_agent/models/requests.py +500 -0
- django_cfg/modules/django_app_agent/models/responses.py +585 -0
- django_cfg/modules/django_app_agent/pytest.ini +6 -0
- django_cfg/modules/django_app_agent/services/__init__.py +42 -0
- django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
- django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
- django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
- django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
- django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
- django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
- django_cfg/modules/django_app_agent/services/base.py +437 -0
- django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
- django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
- django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
- django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
- django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
- django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
- django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
- django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
- django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
- django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
- django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
- django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
- django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
- django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
- django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
- django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
- django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
- django_cfg/modules/django_app_agent/services/report_service.py +332 -0
- django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
- django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
- django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
- django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
- django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
- django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
- django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
- django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
- django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
- django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
- django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
- django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
- django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
- django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
- django_cfg/modules/django_app_agent/ui/cli.py +419 -0
- django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
- django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
- django_cfg/modules/django_app_agent/utils/logging.py +360 -0
- django_cfg/modules/django_app_agent/utils/validation.py +417 -0
- django_cfg/modules/django_currency/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
- django_cfg/modules/django_currency/core/converter.py +12 -12
- django_cfg/modules/django_currency/database/__init__.py +2 -2
- django_cfg/modules/django_currency/database/database_loader.py +93 -42
- django_cfg/modules/django_llm/llm/client.py +10 -2
- django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
- django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
- django_cfg/modules/django_unfold/dashboard.py +14 -13
- django_cfg/modules/django_unfold/models/config.py +1 -1
- django_cfg/registry/core.py +3 -0
- django_cfg/registry/third_party.py +2 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/RECORD +223 -117
- django_cfg/apps/accounts/admin/activity.py +0 -96
- django_cfg/apps/accounts/admin/group.py +0 -17
- django_cfg/apps/accounts/admin/otp.py +0 -59
- django_cfg/apps/accounts/admin/registration_source.py +0 -97
- django_cfg/apps/accounts/admin/twilio_response.py +0 -227
- django_cfg/apps/accounts/admin/user.py +0 -300
- django_cfg/apps/agents/core/agent.py +0 -281
- django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
- django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
- django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
- django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
- django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
- django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
- django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
- django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
- django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
- django_cfg/apps/tasks/admin.py +0 -320
- django_cfg/middleware/static_nocache.py +0 -55
- django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
- /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.7.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
|
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.
|
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 ["
|
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
|
104
|
-
if self.
|
103
|
+
# Try Hybrid client first (multiple sources with fallback)
|
104
|
+
if self.hybrid.supports_pair(base, quote):
|
105
105
|
try:
|
106
|
-
rate = self.
|
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"
|
110
|
+
logger.warning(f"Hybrid client failed for {base}/{quote}: {e}")
|
111
111
|
|
112
|
-
# Try
|
113
|
-
if self.
|
112
|
+
# Try CoinPaprika next (excellent for crypto, no rate limits)
|
113
|
+
if self.coinpaprika.supports_pair(base, quote):
|
114
114
|
try:
|
115
|
-
rate = self.
|
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"
|
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
|
-
"
|
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
|
-
|
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
|
-
'
|
20
|
+
'HybridCurrencyInfo',
|
21
21
|
'CurrencyRateInfo',
|
22
22
|
'RateLimiter',
|
23
23
|
'create_database_loader',
|