django-cfg 1.2.29__py3-none-any.whl → 1.3.1__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 (258) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/health/views.py +4 -2
  3. django_cfg/apps/knowbase/config/settings.py +16 -15
  4. django_cfg/apps/payments/README.md +326 -0
  5. django_cfg/apps/payments/admin/__init__.py +20 -9
  6. django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
  7. django_cfg/apps/payments/admin/balance_admin.py +592 -297
  8. django_cfg/apps/payments/admin/currencies_admin.py +600 -108
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +470 -64
  11. django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
  12. django_cfg/apps/payments/admin_interface/__init__.py +18 -0
  13. django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
  14. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
  23. django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
  24. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
  25. django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
  26. django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
  27. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
  28. django_cfg/apps/payments/apps.py +34 -9
  29. django_cfg/apps/payments/config/__init__.py +28 -51
  30. django_cfg/apps/payments/config/constance/__init__.py +22 -0
  31. django_cfg/apps/payments/config/constance/config_service.py +123 -0
  32. django_cfg/apps/payments/config/constance/fields.py +69 -0
  33. django_cfg/apps/payments/config/constance/settings.py +160 -0
  34. django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
  35. django_cfg/apps/payments/config/helpers.py +130 -0
  36. django_cfg/apps/payments/management/__init__.py +1 -3
  37. django_cfg/apps/payments/management/commands/__init__.py +1 -3
  38. django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
  39. django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
  40. django_cfg/apps/payments/middleware/__init__.py +3 -1
  41. django_cfg/apps/payments/middleware/api_access.py +329 -222
  42. django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
  43. django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +16 -20
  46. django_cfg/apps/payments/models/api_keys.py +121 -43
  47. django_cfg/apps/payments/models/balance.py +150 -115
  48. django_cfg/apps/payments/models/base.py +68 -15
  49. django_cfg/apps/payments/models/currencies.py +207 -67
  50. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  51. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  52. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  53. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  54. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  55. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  56. django_cfg/apps/payments/models/payments.py +235 -284
  57. django_cfg/apps/payments/models/subscriptions.py +257 -177
  58. django_cfg/apps/payments/models/tariffs.py +147 -40
  59. django_cfg/apps/payments/services/__init__.py +209 -56
  60. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  61. django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
  62. django_cfg/apps/payments/services/core/__init__.py +10 -6
  63. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  64. django_cfg/apps/payments/services/core/base.py +166 -0
  65. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  66. django_cfg/apps/payments/services/core/payment_service.py +344 -468
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -484
  68. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  69. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  70. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  71. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  72. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  73. django_cfg/apps/payments/services/providers/base.py +232 -71
  74. django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
  75. django_cfg/apps/payments/services/providers/registry.py +429 -80
  76. django_cfg/apps/payments/services/types/__init__.py +78 -0
  77. django_cfg/apps/payments/services/types/data.py +177 -0
  78. django_cfg/apps/payments/services/types/requests.py +150 -0
  79. django_cfg/apps/payments/services/types/responses.py +156 -0
  80. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  81. django_cfg/apps/payments/signals/__init__.py +33 -8
  82. django_cfg/apps/payments/signals/api_key_signals.py +211 -130
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +129 -98
  85. django_cfg/apps/payments/signals/subscription_signals.py +195 -143
  86. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  87. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  88. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  89. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  90. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  91. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  92. django_cfg/apps/payments/urls.py +46 -47
  93. django_cfg/apps/payments/urls_admin.py +49 -0
  94. django_cfg/apps/payments/views/api/__init__.py +101 -0
  95. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  96. django_cfg/apps/payments/views/api/balances.py +381 -0
  97. django_cfg/apps/payments/views/api/base.py +298 -0
  98. django_cfg/apps/payments/views/api/currencies.py +402 -0
  99. django_cfg/apps/payments/views/api/payments.py +415 -0
  100. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  101. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  102. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  103. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  104. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  105. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  106. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  107. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  108. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  109. django_cfg/apps/tasks/urls.py +0 -2
  110. django_cfg/apps/tasks/urls_admin.py +14 -0
  111. django_cfg/apps/urls.py +4 -4
  112. django_cfg/config.py +1 -1
  113. django_cfg/core/config.py +75 -4
  114. django_cfg/core/generation.py +25 -4
  115. django_cfg/core/integration/README.md +363 -0
  116. django_cfg/core/integration/__init__.py +47 -0
  117. django_cfg/core/integration/commands_collector.py +239 -0
  118. django_cfg/core/integration/display/__init__.py +15 -0
  119. django_cfg/core/integration/display/base.py +157 -0
  120. django_cfg/core/integration/display/ngrok.py +164 -0
  121. django_cfg/core/integration/display/startup.py +815 -0
  122. django_cfg/core/integration/url_integration.py +123 -0
  123. django_cfg/core/integration/version_checker.py +160 -0
  124. django_cfg/management/commands/auto_generate.py +4 -0
  125. django_cfg/management/commands/check_settings.py +6 -0
  126. django_cfg/management/commands/clear_constance.py +5 -2
  127. django_cfg/management/commands/create_token.py +6 -0
  128. django_cfg/management/commands/list_urls.py +6 -0
  129. django_cfg/management/commands/migrate_all.py +6 -0
  130. django_cfg/management/commands/migrator.py +3 -0
  131. django_cfg/management/commands/rundramatiq.py +6 -0
  132. django_cfg/management/commands/runserver_ngrok.py +51 -29
  133. django_cfg/management/commands/script.py +6 -0
  134. django_cfg/management/commands/show_config.py +12 -2
  135. django_cfg/management/commands/show_urls.py +4 -0
  136. django_cfg/management/commands/superuser.py +6 -0
  137. django_cfg/management/commands/task_clear.py +4 -1
  138. django_cfg/management/commands/task_status.py +3 -1
  139. django_cfg/management/commands/test_email.py +3 -0
  140. django_cfg/management/commands/test_telegram.py +6 -0
  141. django_cfg/management/commands/test_twilio.py +6 -0
  142. django_cfg/management/commands/tree.py +6 -0
  143. django_cfg/management/commands/validate_config.py +155 -149
  144. django_cfg/models/constance.py +31 -11
  145. django_cfg/models/payments.py +175 -498
  146. django_cfg/modules/django_currency/__init__.py +16 -11
  147. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  148. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  149. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  150. django_cfg/modules/django_currency/core/__init__.py +1 -7
  151. django_cfg/modules/django_currency/core/converter.py +18 -23
  152. django_cfg/modules/django_currency/core/models.py +122 -11
  153. django_cfg/modules/django_currency/database/__init__.py +4 -4
  154. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  155. django_cfg/modules/django_logger.py +160 -146
  156. django_cfg/modules/django_unfold/dashboard.py +65 -12
  157. django_cfg/registry/core.py +1 -0
  158. django_cfg/template_archive/django_sample.zip +0 -0
  159. django_cfg/templates/admin/components/action_grid.html +9 -9
  160. django_cfg/templates/admin/components/metric_card.html +5 -5
  161. django_cfg/templates/admin/components/status_badge.html +2 -2
  162. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  163. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  164. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  165. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  166. django_cfg/utils/smart_defaults.py +222 -571
  167. django_cfg/utils/toolkit.py +51 -11
  168. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
  169. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
  170. django_cfg/apps/payments/__init__.py +0 -8
  171. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  172. django_cfg/apps/payments/config/module.py +0 -70
  173. django_cfg/apps/payments/config/providers.py +0 -105
  174. django_cfg/apps/payments/config/settings.py +0 -96
  175. django_cfg/apps/payments/config/utils.py +0 -52
  176. django_cfg/apps/payments/decorators.py +0 -291
  177. django_cfg/apps/payments/management/commands/README.md +0 -178
  178. django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
  179. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  180. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  181. django_cfg/apps/payments/managers/__init__.py +0 -22
  182. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  183. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  184. django_cfg/apps/payments/managers/currency_manager.py +0 -83
  185. django_cfg/apps/payments/managers/payment_manager.py +0 -44
  186. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  187. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  188. django_cfg/apps/payments/models/events.py +0 -73
  189. django_cfg/apps/payments/serializers/__init__.py +0 -56
  190. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  191. django_cfg/apps/payments/serializers/balance.py +0 -59
  192. django_cfg/apps/payments/serializers/currencies.py +0 -55
  193. django_cfg/apps/payments/serializers/payments.py +0 -62
  194. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  195. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  196. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  197. django_cfg/apps/payments/services/cache/base.py +0 -30
  198. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  199. django_cfg/apps/payments/services/internal_types.py +0 -297
  200. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  201. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  202. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
  203. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  204. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  205. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  206. django_cfg/apps/payments/services/security/__init__.py +0 -34
  207. django_cfg/apps/payments/services/security/error_handler.py +0 -637
  208. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  209. django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
  210. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  211. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  212. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  213. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  214. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  215. django_cfg/apps/payments/tasks/__init__.py +0 -12
  216. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  217. django_cfg/apps/payments/templates/payments/base.html +0 -182
  218. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  219. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  220. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
  221. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  222. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
  223. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
  224. django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
  225. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  226. django_cfg/apps/payments/urls_templates.py +0 -52
  227. django_cfg/apps/payments/utils/__init__.py +0 -45
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -245
  230. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  231. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  232. django_cfg/apps/payments/views/__init__.py +0 -62
  233. django_cfg/apps/payments/views/api_key_views.py +0 -164
  234. django_cfg/apps/payments/views/balance_views.py +0 -75
  235. django_cfg/apps/payments/views/currency_views.py +0 -111
  236. django_cfg/apps/payments/views/payment_views.py +0 -149
  237. django_cfg/apps/payments/views/subscription_views.py +0 -135
  238. django_cfg/apps/payments/views/tariff_views.py +0 -131
  239. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  240. django_cfg/apps/payments/views/templates/ajax.py +0 -312
  241. django_cfg/apps/payments/views/templates/base.py +0 -204
  242. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  243. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  244. django_cfg/apps/payments/views/templates/payment_management.py +0 -164
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -240
  247. django_cfg/apps/payments/views/templates/utils.py +0 -181
  248. django_cfg/apps/payments/views/webhook_views.py +0 -266
  249. django_cfg/apps/payments/viewsets.py +0 -65
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  252. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  253. django_cfg/template_archive/.gitignore +0 -1
  254. django_cfg/template_archive/__init__.py +0 -0
  255. django_cfg/urls.py +0 -33
  256. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  257. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  258. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -11,9 +11,6 @@ from .core import (
11
11
  Rate,
12
12
  ConversionRequest,
13
13
  ConversionResult,
14
- SupportedCurrencies,
15
- YFinanceCurrencies,
16
- CoinGeckoCurrencies,
17
14
  CurrencyError,
18
15
  CurrencyNotFoundError,
19
16
  RateFetchError,
@@ -25,7 +22,7 @@ from .core import (
25
22
  from .utils import CacheManager
26
23
 
27
24
  # Clients
28
- from .clients import YFinanceClient, CoinGeckoClient
25
+ from .clients import YahooFinanceClient, CoinPaprikaClient
29
26
 
30
27
  # Database tools
31
28
  from .database import (
@@ -35,6 +32,17 @@ from .database import (
35
32
  load_currencies_to_database_format
36
33
  )
37
34
 
35
+ # Shared global converter instance for caching efficiency
36
+ _global_converter = None
37
+
38
+ def _get_converter() -> CurrencyConverter:
39
+ """Get or create shared converter instance."""
40
+ global _global_converter
41
+ if _global_converter is None:
42
+ _global_converter = CurrencyConverter(cache_ttl=3600) # 1 hour cache
43
+ return _global_converter
44
+
45
+
38
46
  # Simple public API
39
47
  def convert_currency(amount: float, from_currency: str, to_currency: str) -> float:
40
48
  """
@@ -48,7 +56,7 @@ def convert_currency(amount: float, from_currency: str, to_currency: str) -> flo
48
56
  Returns:
49
57
  Converted amount
50
58
  """
51
- converter = CurrencyConverter()
59
+ converter = _get_converter()
52
60
  result = converter.convert(amount, from_currency, to_currency)
53
61
  return result.result
54
62
 
@@ -64,7 +72,7 @@ def get_exchange_rate(base: str, quote: str) -> float:
64
72
  Returns:
65
73
  Exchange rate
66
74
  """
67
- converter = CurrencyConverter()
75
+ converter = _get_converter()
68
76
  result = converter.convert(1.0, base, quote)
69
77
  return result.rate.rate
70
78
 
@@ -75,9 +83,6 @@ __all__ = [
75
83
  "Rate",
76
84
  "ConversionRequest",
77
85
  "ConversionResult",
78
- "SupportedCurrencies",
79
- "YFinanceCurrencies",
80
- "CoinGeckoCurrencies",
81
86
 
82
87
  # Exceptions
83
88
  "CurrencyError",
@@ -90,8 +95,8 @@ __all__ = [
90
95
  "CacheManager",
91
96
 
92
97
  # Clients
93
- "YFinanceClient",
94
- "CoinGeckoClient",
98
+ "YahooFinanceClient",
99
+ "CoinPaprikaClient",
95
100
 
96
101
  # Database tools
97
102
  "CurrencyDatabaseLoader",
@@ -2,10 +2,10 @@
2
2
  Currency data clients for fetching rates from external APIs.
3
3
  """
4
4
 
5
- from .yfinance_client import YFinanceClient
6
- from .coingecko_client import CoinGeckoClient
5
+ from .yahoo_client import YahooFinanceClient
6
+ from .coinpaprika_client import CoinPaprikaClient
7
7
 
8
8
  __all__ = [
9
- 'YFinanceClient',
10
- 'CoinGeckoClient'
9
+ 'YahooFinanceClient',
10
+ 'CoinPaprikaClient'
11
11
  ]
@@ -0,0 +1,289 @@
1
+ """
2
+ CoinPaprika client for crypto rates - much simpler and more reliable than CoinGecko.
3
+
4
+ CoinPaprika API provides all crypto rates in a single request without rate limits.
5
+ """
6
+
7
+ import logging
8
+ import time
9
+ import requests
10
+ from datetime import datetime
11
+ from typing import Dict, Set, Optional, List
12
+ from cachetools import TTLCache
13
+
14
+ from ..core.models import Rate, CoinPaprikaTicker, CoinPaprikaTickersResponse
15
+ from ..core.exceptions import RateFetchError
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class CoinPaprikaClient:
21
+ """Client for fetching crypto rates from CoinPaprika API."""
22
+
23
+ def __init__(self, cache_ttl: int = 600):
24
+ """Initialize CoinPaprika client with TTL cache."""
25
+ self.base_url = "https://api.coinpaprika.com/v1"
26
+ self._rate_cache = TTLCache(maxsize=5000, ttl=cache_ttl) # Cache rates for 10 minutes
27
+ self._all_rates_cache = TTLCache(maxsize=1, ttl=300) # Cache all rates for 5 minutes
28
+ self._session = requests.Session()
29
+ self._session.headers.update({
30
+ 'User-Agent': 'django-cfg-currency-client/1.0',
31
+ 'Accept': 'application/json'
32
+ })
33
+
34
+ def fetch_rate(self, base: str, quote: str) -> Rate:
35
+ """
36
+ Fetch crypto exchange rate from CoinPaprika.
37
+
38
+ Args:
39
+ base: Base currency code (crypto)
40
+ quote: Quote currency code (usually USD)
41
+
42
+ Returns:
43
+ Rate object with exchange rate data
44
+
45
+ Raises:
46
+ RateFetchError: If rate fetch fails
47
+ """
48
+ if quote.upper() != 'USD':
49
+ raise RateFetchError(f"CoinPaprika only supports USD quotes, got {quote}")
50
+
51
+ cache_key = f"{base.upper()}_{quote.upper()}"
52
+
53
+ # Try cache first
54
+ if cache_key in self._rate_cache:
55
+ logger.debug(f"Retrieved rate {base}/{quote} from cache")
56
+ return self._rate_cache[cache_key]
57
+
58
+ try:
59
+ # Get all rates and find our currency
60
+ all_rates = self._fetch_all_rates()
61
+
62
+ base_upper = base.upper()
63
+ for ticker in all_rates:
64
+ if ticker.symbol == base_upper:
65
+ price = ticker.quotes.USD.price
66
+
67
+ # Parse ISO format: 2021-01-01T00:00:00Z
68
+ timestamp = datetime.fromisoformat(ticker.last_updated.replace('Z', '+00:00'))
69
+
70
+ rate = Rate(
71
+ source="coinpaprika",
72
+ base_currency=base.upper(),
73
+ quote_currency="USD",
74
+ rate=float(price),
75
+ timestamp=timestamp
76
+ )
77
+
78
+ # Cache the result
79
+ self._rate_cache[cache_key] = rate
80
+
81
+ return rate
82
+
83
+ raise RateFetchError(f"Currency {base} not found in CoinPaprika data")
84
+
85
+ except Exception as e:
86
+ logger.error(f"Failed to fetch rate for {base}/{quote}: {e}")
87
+ raise RateFetchError(f"CoinPaprika fetch failed: {e}")
88
+
89
+ def _fetch_all_tickers(self) -> Dict[str, dict]:
90
+ """
91
+ Fetch all tickers from CoinPaprika API.
92
+
93
+ Returns:
94
+ Dict with symbol as key and ticker data as value
95
+ """
96
+ cache_key = "all_tickers"
97
+ if cache_key in self._all_rates_cache:
98
+ logger.debug("Retrieved all tickers from CoinPaprika cache")
99
+ return self._all_rates_cache[cache_key]
100
+
101
+ try:
102
+ response = requests.get(f"{self.base_url}/tickers")
103
+ response.raise_for_status()
104
+ tickers_data = response.json()
105
+
106
+ # Process data into a more accessible format: {symbol: {id: ..., price: ...}}
107
+ processed_tickers = {}
108
+ for ticker in tickers_data:
109
+ symbol = ticker['symbol'].upper()
110
+ processed_tickers[symbol] = {
111
+ 'id': ticker['id'],
112
+ 'name': ticker['name'],
113
+ 'price_usd': ticker['quotes']['USD']['price'] if 'USD' in ticker['quotes'] else None,
114
+ 'last_updated': ticker['last_updated']
115
+ }
116
+
117
+ self._all_rates_cache[cache_key] = processed_tickers
118
+ logger.info(f"Fetched and cached {len(processed_tickers)} tickers from CoinPaprika")
119
+ return processed_tickers
120
+ except requests.exceptions.RequestException as e:
121
+ logger.error(f"Failed to fetch all tickers from CoinPaprika: {e}")
122
+ raise RateFetchError(f"CoinPaprika API error: {e}")
123
+
124
+ def _fetch_all_rates(self) -> List[CoinPaprikaTicker]:
125
+ """
126
+ Fetch all cryptocurrency rates from CoinPaprika.
127
+
128
+ Returns:
129
+ List of CoinPaprikaTicker objects
130
+ """
131
+ cache_key = "all_rates"
132
+
133
+ # Try cache first
134
+ if cache_key in self._all_rates_cache:
135
+ logger.debug("Retrieved all rates from cache")
136
+ return self._all_rates_cache[cache_key]
137
+
138
+ try:
139
+ url = f"{self.base_url}/tickers"
140
+ logger.debug(f"Fetching all rates from {url}")
141
+
142
+ response = self._session.get(url, timeout=30)
143
+ response.raise_for_status()
144
+
145
+ raw_data = response.json()
146
+
147
+ # Validate response using Pydantic model
148
+ try:
149
+ tickers_response = CoinPaprikaTickersResponse(raw_data)
150
+ tickers = tickers_response.root
151
+ except Exception as e:
152
+ raise RateFetchError(f"Invalid CoinPaprika response format: {e}")
153
+
154
+ # Cache the result
155
+ self._all_rates_cache[cache_key] = tickers
156
+ logger.info(f"Fetched {len(tickers)} cryptocurrencies from CoinPaprika")
157
+
158
+ return tickers
159
+
160
+ except requests.RequestException as e:
161
+ logger.error(f"HTTP error fetching from CoinPaprika: {e}")
162
+ raise RateFetchError(f"Failed to fetch data from CoinPaprika: {e}")
163
+ except Exception as e:
164
+ logger.error(f"Unexpected error fetching from CoinPaprika: {e}")
165
+ raise RateFetchError(f"CoinPaprika fetch failed: {e}")
166
+
167
+ def get_supported_cryptocurrencies(self) -> Set[str]:
168
+ """
169
+ Get all supported cryptocurrency symbols.
170
+
171
+ Returns:
172
+ Set of supported crypto symbols
173
+ """
174
+ try:
175
+ all_rates = self._fetch_all_rates()
176
+ symbols = {ticker.symbol for ticker in all_rates}
177
+ logger.debug(f"Found {len(symbols)} supported cryptocurrencies")
178
+ return symbols
179
+ except Exception as e:
180
+ logger.error(f"Failed to get supported cryptocurrencies: {e}")
181
+ return set()
182
+
183
+ def get_all_supported_currencies(self) -> Dict[str, str]:
184
+ """Get all supported cryptocurrencies from CoinPaprika."""
185
+ all_tickers = self._fetch_all_tickers()
186
+ return {symbol: data['name'] for symbol, data in all_tickers.items() if data['price_usd'] is not None}
187
+
188
+ def supports_pair(self, base: str, quote: str) -> bool:
189
+ """
190
+ Check if a currency pair is supported.
191
+
192
+ Args:
193
+ base: Base currency code
194
+ quote: Quote currency code
195
+
196
+ Returns:
197
+ True if supported, False otherwise
198
+ """
199
+ if quote.upper() != 'USD':
200
+ return False
201
+
202
+ supported_cryptos = self.get_supported_cryptocurrencies()
203
+ return base.upper() in supported_cryptos
204
+
205
+ def fetch_multiple_rates(self, currency_codes: List[str], quote: str = 'USD') -> Dict[str, Rate]:
206
+ """
207
+ Fetch multiple cryptocurrency rates efficiently.
208
+
209
+ Args:
210
+ currency_codes: List of crypto currency codes
211
+ quote: Quote currency (default: USD)
212
+
213
+ Returns:
214
+ Dictionary mapping currency codes to Rate objects
215
+ """
216
+ if quote.upper() != 'USD':
217
+ raise RateFetchError(f"CoinPaprika only supports USD quotes, got {quote}")
218
+
219
+ results = {}
220
+
221
+ try:
222
+ # Fetch all rates once
223
+ all_rates = self._fetch_all_rates()
224
+
225
+ # Create lookup dictionary
226
+ rates_by_symbol = {ticker.symbol: ticker for ticker in all_rates}
227
+
228
+ # Process requested currencies
229
+ for currency_code in currency_codes:
230
+ currency_upper = currency_code.upper()
231
+
232
+ if currency_upper in rates_by_symbol:
233
+ ticker = rates_by_symbol[currency_upper]
234
+ price = ticker.quotes.USD.price
235
+
236
+ # Parse ISO format: 2021-01-01T00:00:00Z
237
+ timestamp = datetime.fromisoformat(ticker.last_updated.replace('Z', '+00:00'))
238
+
239
+ rate = Rate(
240
+ source="coinpaprika",
241
+ base_currency=currency_upper,
242
+ quote_currency="USD",
243
+ rate=float(price),
244
+ timestamp=timestamp
245
+ )
246
+
247
+ results[currency_upper] = rate
248
+
249
+ # Cache individual rate
250
+ cache_key = f"{currency_upper}_USD"
251
+ self._rate_cache[cache_key] = rate
252
+ else:
253
+ logger.warning(f"Currency {currency_code} not found in CoinPaprika data")
254
+
255
+ logger.info(f"Successfully fetched {len(results)} rates from CoinPaprika")
256
+ return results
257
+
258
+ except Exception as e:
259
+ logger.error(f"Failed to fetch multiple rates: {e}")
260
+ raise RateFetchError(f"CoinPaprika batch fetch failed: {e}")
261
+
262
+ def get_top_cryptocurrencies(self, limit: int = 100) -> List[Dict]:
263
+ """
264
+ Get top cryptocurrencies by market cap rank.
265
+
266
+ Args:
267
+ limit: Maximum number of currencies to return
268
+
269
+ Returns:
270
+ List of cryptocurrency data dictionaries
271
+ """
272
+ try:
273
+ all_rates = self._fetch_all_rates()
274
+
275
+ # Filter and sort by rank
276
+ valid_tickers = [
277
+ ticker for ticker in all_rates
278
+ if ticker.rank and ticker.quotes.USD.price
279
+ ]
280
+
281
+ # Sort by rank and limit
282
+ top_tickers = sorted(valid_tickers, key=lambda x: x.rank)[:limit]
283
+
284
+ logger.info(f"Retrieved top {len(top_tickers)} cryptocurrencies")
285
+ return top_tickers
286
+
287
+ except Exception as e:
288
+ logger.error(f"Failed to get top cryptocurrencies: {e}")
289
+ return []
@@ -0,0 +1,157 @@
1
+ import logging
2
+ import requests
3
+ import time
4
+ from datetime import datetime
5
+ from typing import Dict, Set, Optional
6
+ from cachetools import TTLCache
7
+
8
+ from ..core.models import Rate, YahooFinanceResponse
9
+ from ..core.exceptions import RateFetchError
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class YahooFinanceClient:
15
+ """Simple Yahoo Finance client without yfinance dependency."""
16
+
17
+ BASE_URL = "https://query1.finance.yahoo.com/v8/finance/chart"
18
+
19
+ def __init__(self, cache_ttl: int = 3600):
20
+ """Initialize Yahoo Finance client with TTL cache."""
21
+ self._rate_cache = TTLCache(maxsize=500, ttl=cache_ttl)
22
+ self._session = requests.Session()
23
+ self._session.headers.update({
24
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
25
+ })
26
+ self._last_request_time = 0
27
+ self._rate_limit_delay = 1.0 # 1 second between requests
28
+
29
+ def _get_yahoo_symbol(self, base: str, quote: str) -> str:
30
+ """Convert currency pair to Yahoo Finance symbol format."""
31
+ # Yahoo uses format like EURUSD=X for forex pairs
32
+ return f"{base}{quote}=X"
33
+
34
+ def fetch_rate(self, base: str, quote: str) -> Rate:
35
+ """
36
+ Fetch forex rate from Yahoo Finance with caching.
37
+
38
+ Args:
39
+ base: Base currency code (e.g., EUR)
40
+ quote: Quote currency code (e.g., USD)
41
+
42
+ Returns:
43
+ Rate object with exchange rate data
44
+
45
+ Raises:
46
+ RateFetchError: If rate fetch fails
47
+ """
48
+ base = base.upper()
49
+ quote = quote.upper()
50
+ cache_key = f"{base}_{quote}"
51
+
52
+ # Try cache first
53
+ if cache_key in self._rate_cache:
54
+ logger.debug(f"Retrieved rate {base}/{quote} from Yahoo cache")
55
+ return self._rate_cache[cache_key]
56
+
57
+ symbol = self._get_yahoo_symbol(base, quote)
58
+
59
+ # Rate limiting
60
+ current_time = time.time()
61
+ time_since_last_request = current_time - self._last_request_time
62
+ if time_since_last_request < self._rate_limit_delay:
63
+ sleep_time = self._rate_limit_delay - time_since_last_request
64
+ logger.debug(f"Rate limiting: sleeping for {sleep_time:.2f}s")
65
+ time.sleep(sleep_time)
66
+
67
+ try:
68
+ response = self._session.get(f"{self.BASE_URL}/{symbol}")
69
+ self._last_request_time = time.time()
70
+ response.raise_for_status()
71
+
72
+ raw_data = response.json()
73
+
74
+ # Validate response using Pydantic model
75
+ try:
76
+ yahoo_response = YahooFinanceResponse(**raw_data)
77
+ except Exception as e:
78
+ raise RateFetchError(f"Invalid Yahoo Finance response format: {e}")
79
+
80
+ if not yahoo_response.chart.result:
81
+ raise RateFetchError(f"No data returned for {symbol}")
82
+
83
+ meta = yahoo_response.chart.result[0].meta
84
+ rate_value = meta.regularMarketPrice
85
+ timestamp = datetime.fromtimestamp(meta.regularMarketTime)
86
+
87
+ rate = Rate(
88
+ source="yahoo",
89
+ base_currency=base,
90
+ quote_currency=quote,
91
+ rate=float(rate_value),
92
+ timestamp=timestamp
93
+ )
94
+
95
+ self._rate_cache[cache_key] = rate
96
+ logger.info(f"Fetched rate {base}/{quote} = {rate_value} from Yahoo Finance")
97
+ return rate
98
+
99
+ except requests.exceptions.RequestException as e:
100
+ logger.error(f"Failed to fetch rate from Yahoo Finance: {e}")
101
+ raise RateFetchError(f"Yahoo Finance API error: {e}")
102
+ except (KeyError, TypeError, ValueError) as e:
103
+ logger.error(f"Failed to parse Yahoo Finance response: {e}")
104
+ raise RateFetchError(f"Invalid response format: {e}")
105
+ except Exception as e:
106
+ logger.error(f"Unexpected error fetching from Yahoo Finance: {e}")
107
+ raise RateFetchError(f"Yahoo Finance fetch failed: {e}")
108
+
109
+ def supports_pair(self, base: str, quote: str) -> bool:
110
+ """
111
+ Check if Yahoo Finance supports the given currency pair.
112
+
113
+ Yahoo Finance primarily supports major forex pairs.
114
+ """
115
+ base = base.upper()
116
+ quote = quote.upper()
117
+
118
+ # Major currencies supported by Yahoo Finance
119
+ major_currencies = {
120
+ 'USD', 'EUR', 'GBP', 'JPY', 'CHF', 'CAD', 'AUD', 'NZD',
121
+ 'SEK', 'NOK', 'DKK', 'PLN', 'CZK', 'HUF', 'RUB', 'CNY',
122
+ 'INR', 'KRW', 'SGD', 'HKD', 'THB', 'MXN', 'BRL', 'ZAR',
123
+ 'TRY', 'ILS'
124
+ }
125
+
126
+ return base in major_currencies and quote in major_currencies
127
+
128
+ def get_all_supported_currencies(self) -> Dict[str, str]:
129
+ """Get all major currencies supported by Yahoo Finance."""
130
+ return {
131
+ 'USD': 'US Dollar',
132
+ 'EUR': 'Euro',
133
+ 'GBP': 'British Pound',
134
+ 'JPY': 'Japanese Yen',
135
+ 'CHF': 'Swiss Franc',
136
+ 'CAD': 'Canadian Dollar',
137
+ 'AUD': 'Australian Dollar',
138
+ 'NZD': 'New Zealand Dollar',
139
+ 'SEK': 'Swedish Krona',
140
+ 'NOK': 'Norwegian Krone',
141
+ 'DKK': 'Danish Krone',
142
+ 'PLN': 'Polish Zloty',
143
+ 'CZK': 'Czech Koruna',
144
+ 'HUF': 'Hungarian Forint',
145
+ 'RUB': 'Russian Ruble',
146
+ 'CNY': 'Chinese Yuan',
147
+ 'INR': 'Indian Rupee',
148
+ 'KRW': 'South Korean Won',
149
+ 'SGD': 'Singapore Dollar',
150
+ 'HKD': 'Hong Kong Dollar',
151
+ 'THB': 'Thai Baht',
152
+ 'MXN': 'Mexican Peso',
153
+ 'BRL': 'Brazilian Real',
154
+ 'ZAR': 'South African Rand',
155
+ 'TRY': 'Turkish Lira',
156
+ 'ILS': 'Israeli Shekel'
157
+ }
@@ -5,10 +5,7 @@ Core currency conversion functionality.
5
5
  from .models import (
6
6
  Rate,
7
7
  ConversionRequest,
8
- ConversionResult,
9
- YFinanceCurrencies,
10
- CoinGeckoCurrencies,
11
- SupportedCurrencies
8
+ ConversionResult
12
9
  )
13
10
 
14
11
  from .exceptions import (
@@ -26,9 +23,6 @@ __all__ = [
26
23
  'Rate',
27
24
  'ConversionRequest',
28
25
  'ConversionResult',
29
- 'YFinanceCurrencies',
30
- 'CoinGeckoCurrencies',
31
- 'SupportedCurrencies',
32
26
 
33
27
  # Exceptions
34
28
  'CurrencyError',
@@ -5,9 +5,9 @@ Main currency converter with intelligent routing.
5
5
  import logging
6
6
  from typing import Optional
7
7
 
8
- from .models import Rate, ConversionRequest, ConversionResult, SupportedCurrencies, YFinanceCurrencies, CoinGeckoCurrencies
8
+ from .models import Rate, ConversionRequest, ConversionResult
9
9
  from .exceptions import ConversionError, CurrencyNotFoundError
10
- from ..clients import YFinanceClient, CoinGeckoClient
10
+ from ..clients import YahooFinanceClient, CoinPaprikaClient
11
11
  from ..utils.cache import CacheManager
12
12
 
13
13
  logger = logging.getLogger(__name__)
@@ -23,8 +23,8 @@ class CurrencyConverter:
23
23
  Args:
24
24
  cache_ttl: Cache TTL in seconds
25
25
  """
26
- self.yfinance = YFinanceClient()
27
- self.coingecko = CoinGeckoClient()
26
+ self.yahoo = YahooFinanceClient(cache_ttl=cache_ttl)
27
+ self.coinpaprika = CoinPaprikaClient(cache_ttl=cache_ttl)
28
28
  self.cache = CacheManager(ttl=cache_ttl)
29
29
 
30
30
  def convert(self, amount: float, from_currency: str, to_currency: str) -> ConversionResult:
@@ -95,28 +95,28 @@ class CurrencyConverter:
95
95
  CurrencyNotFoundError: If no provider supports the pair
96
96
  """
97
97
  # Try cache first
98
- for source in ["yfinance", "coingecko"]:
98
+ for source in ["yahoo", "coinpaprika"]:
99
99
  cached_rate = self.cache.get_rate(base, quote, source)
100
100
  if cached_rate:
101
101
  return cached_rate
102
102
 
103
- # Try YFinance first (good for fiat and major crypto)
104
- if self.yfinance.supports_pair(base, quote):
103
+ # Try CoinPaprika first (excellent for crypto, no rate limits)
104
+ if self.coinpaprika.supports_pair(base, quote):
105
105
  try:
106
- rate = self.yfinance.fetch_rate(base, quote)
106
+ rate = self.coinpaprika.fetch_rate(base, quote)
107
107
  self.cache.set_rate(rate)
108
108
  return rate
109
109
  except Exception as e:
110
- logger.warning(f"YFinance failed for {base}/{quote}: {e}")
110
+ logger.warning(f"CoinPaprika failed for {base}/{quote}: {e}")
111
111
 
112
- # Try CoinGecko (good for crypto)
113
- if self.coingecko.supports_pair(base, quote):
112
+ # Try Yahoo Finance next (good for fiat and major forex pairs)
113
+ if self.yahoo.supports_pair(base, quote):
114
114
  try:
115
- rate = self.coingecko.fetch_rate(base, quote)
115
+ rate = self.yahoo.fetch_rate(base, quote)
116
116
  self.cache.set_rate(rate)
117
117
  return rate
118
118
  except Exception as e:
119
- logger.warning(f"CoinGecko failed for {base}/{quote}: {e}")
119
+ logger.warning(f"Yahoo Finance failed for {base}/{quote}: {e}")
120
120
 
121
121
  # Try indirect conversion via USD
122
122
  if base != "USD" and quote != "USD":
@@ -156,14 +156,9 @@ class CurrencyConverter:
156
156
  rate=combined_rate
157
157
  )
158
158
 
159
- def get_supported_currencies(self) -> SupportedCurrencies:
159
+ def get_supported_currencies(self) -> dict:
160
160
  """Get list of supported currencies by provider."""
161
- return SupportedCurrencies(
162
- yfinance=YFinanceCurrencies(
163
- fiat=list(self.yfinance.get_fiat_currencies())
164
- ),
165
- coingecko=CoinGeckoCurrencies(
166
- crypto=list(self.coingecko.get_crypto_ids().keys()),
167
- vs_currencies=list(self.coingecko.get_vs_currencies())
168
- )
169
- )
161
+ return {
162
+ "yahoo": self.yahoo.get_all_supported_currencies(),
163
+ "coinpaprika": self.coinpaprika.get_all_supported_currencies()
164
+ }