django-cfg 1.2.22__py3-none-any.whl → 1.2.25__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 (125) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
  3. django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
  4. django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
  5. django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
  6. django_cfg/apps/payments/admin/__init__.py +23 -0
  7. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  8. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  9. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  10. django_cfg/apps/payments/admin/filters.py +259 -0
  11. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  12. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  13. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  14. django_cfg/apps/payments/config/__init__.py +65 -0
  15. django_cfg/apps/payments/config/module.py +70 -0
  16. django_cfg/apps/payments/config/providers.py +115 -0
  17. django_cfg/apps/payments/config/settings.py +96 -0
  18. django_cfg/apps/payments/config/utils.py +52 -0
  19. django_cfg/apps/payments/decorators.py +291 -0
  20. django_cfg/apps/payments/management/__init__.py +3 -0
  21. django_cfg/apps/payments/management/commands/README.md +178 -0
  22. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  23. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  24. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  25. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  26. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  27. django_cfg/apps/payments/middleware/api_access.py +294 -0
  28. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  29. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  30. django_cfg/apps/payments/migrations/0001_initial.py +125 -11
  31. django_cfg/apps/payments/models/__init__.py +18 -0
  32. django_cfg/apps/payments/models/api_keys.py +2 -2
  33. django_cfg/apps/payments/models/balance.py +2 -2
  34. django_cfg/apps/payments/models/base.py +16 -0
  35. django_cfg/apps/payments/models/events.py +2 -2
  36. django_cfg/apps/payments/models/payments.py +112 -2
  37. django_cfg/apps/payments/models/subscriptions.py +2 -2
  38. django_cfg/apps/payments/services/__init__.py +64 -7
  39. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  40. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  41. django_cfg/apps/payments/services/cache/base.py +30 -0
  42. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  43. django_cfg/apps/payments/services/core/__init__.py +17 -0
  44. django_cfg/apps/payments/services/core/balance_service.py +447 -0
  45. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  46. django_cfg/apps/payments/services/core/payment_service.py +576 -0
  47. django_cfg/apps/payments/services/core/subscription_service.py +614 -0
  48. django_cfg/apps/payments/services/internal_types.py +297 -0
  49. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  50. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  51. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  52. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  53. django_cfg/apps/payments/services/providers/__init__.py +22 -0
  54. django_cfg/apps/payments/services/providers/base.py +137 -0
  55. django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
  56. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  57. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  58. django_cfg/apps/payments/services/providers/registry.py +103 -0
  59. django_cfg/apps/payments/services/security/__init__.py +34 -0
  60. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  61. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  62. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  63. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  64. django_cfg/apps/payments/signals/__init__.py +13 -0
  65. django_cfg/apps/payments/signals/api_key_signals.py +160 -0
  66. django_cfg/apps/payments/signals/payment_signals.py +128 -0
  67. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  68. django_cfg/apps/payments/tasks/__init__.py +12 -0
  69. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  70. django_cfg/apps/payments/urls.py +5 -5
  71. django_cfg/apps/payments/utils/__init__.py +45 -0
  72. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  73. django_cfg/apps/payments/utils/config_utils.py +245 -0
  74. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  75. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  76. django_cfg/apps/payments/views/payment_views.py +40 -2
  77. django_cfg/apps/payments/views/webhook_views.py +266 -0
  78. django_cfg/apps/payments/viewsets.py +65 -0
  79. django_cfg/apps/support/signals.py +16 -4
  80. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  81. django_cfg/cli/README.md +2 -2
  82. django_cfg/cli/commands/create_project.py +1 -1
  83. django_cfg/cli/commands/info.py +1 -1
  84. django_cfg/cli/main.py +1 -1
  85. django_cfg/cli/utils.py +5 -5
  86. django_cfg/core/config.py +18 -4
  87. django_cfg/models/payments.py +546 -0
  88. django_cfg/models/revolution.py +1 -1
  89. django_cfg/models/tasks.py +51 -2
  90. django_cfg/modules/base.py +12 -6
  91. django_cfg/modules/django_currency/README.md +104 -269
  92. django_cfg/modules/django_currency/__init__.py +99 -41
  93. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  94. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  95. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  96. django_cfg/modules/django_currency/core/__init__.py +42 -0
  97. django_cfg/modules/django_currency/core/converter.py +169 -0
  98. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  99. django_cfg/modules/django_currency/core/models.py +54 -0
  100. django_cfg/modules/django_currency/database/__init__.py +25 -0
  101. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  102. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  103. django_cfg/modules/django_currency/utils/cache.py +92 -0
  104. django_cfg/modules/django_email.py +42 -4
  105. django_cfg/modules/django_unfold/dashboard.py +20 -0
  106. django_cfg/registry/core.py +10 -0
  107. django_cfg/template_archive/__init__.py +0 -0
  108. django_cfg/template_archive/django_sample.zip +0 -0
  109. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
  110. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
  111. django_cfg/apps/agents/examples/__init__.py +0 -3
  112. django_cfg/apps/agents/examples/simple_example.py +0 -161
  113. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  114. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  115. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  116. django_cfg/apps/payments/services/base.py +0 -68
  117. django_cfg/apps/payments/services/nowpayments.py +0 -78
  118. django_cfg/apps/payments/services/providers.py +0 -77
  119. django_cfg/apps/payments/services/redis_service.py +0 -215
  120. django_cfg/modules/django_currency/cache.py +0 -430
  121. django_cfg/modules/django_currency/converter.py +0 -324
  122. django_cfg/modules/django_currency/service.py +0 -277
  123. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
  124. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
  125. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -1,324 +0,0 @@
1
- """
2
- Currency Converter for django_currency.
3
-
4
- Handles actual currency conversion using multiple data sources with fallback support.
5
- """
6
-
7
- import logging
8
- import requests
9
- from datetime import datetime, date
10
- from typing import Dict, Optional, Union, Any
11
- from pathlib import Path
12
-
13
- from .cache import CurrencyCache
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- class CurrencyConverter:
19
- """
20
- Currency converter with multiple data sources and caching.
21
-
22
- Data sources (in priority order):
23
- 1. Central Bank of Russia (CBR) API - for RUB-based conversions
24
- 2. European Central Bank (ECB) API - for EUR-based conversions
25
- 3. currency_converter library - fallback for other conversions
26
- """
27
-
28
- # API endpoints
29
- CBR_API_URL = "https://www.cbr-xml-daily.ru/daily_json.js"
30
- ECB_API_URL = "https://api.exchangerate-api.com/v4/latest/EUR"
31
-
32
- def __init__(self, cache_dir: Optional[Path] = None):
33
- """
34
- Initialize currency converter.
35
-
36
- Args:
37
- cache_dir: Optional cache directory path
38
- """
39
- self.cache = CurrencyCache(cache_dir=cache_dir)
40
- self._fallback_converter = None
41
- self._cbr_rates = {}
42
- self._ecb_rates = {}
43
-
44
- # Initialize fallback converter
45
- self._init_fallback_converter()
46
-
47
- def _init_fallback_converter(self):
48
- """Initialize fallback currency converter."""
49
- try:
50
- from currency_converter import CurrencyConverter as FallbackConverter
51
- self._fallback_converter = FallbackConverter(
52
- fallback_on_wrong_date=True,
53
- fallback_on_missing_rate=True
54
- )
55
- # Test the converter
56
- _ = self._fallback_converter.convert(1, 'USD', 'EUR')
57
- logger.info("Fallback currency converter initialized")
58
- except ImportError:
59
- logger.warning("currency_converter library not available - install with: pip install CurrencyConverter")
60
- self._fallback_converter = None
61
- except Exception as e:
62
- logger.error(f"Failed to initialize fallback converter: {e}")
63
- self._fallback_converter = None
64
-
65
- def convert(
66
- self,
67
- amount: float,
68
- from_currency: str,
69
- to_currency: str,
70
- date_obj: Optional[Union[datetime, date]] = None,
71
- round_to: Optional[int] = 2,
72
- ) -> float:
73
- """
74
- Convert amount from one currency to another.
75
-
76
- Args:
77
- amount: Amount to convert
78
- from_currency: Source currency code
79
- to_currency: Target currency code
80
- date_obj: Optional date for historical rates
81
- round_to: Number of decimal places to round to
82
-
83
- Returns:
84
- Converted amount
85
- """
86
- from_curr = from_currency.upper()
87
- to_curr = to_currency.upper()
88
-
89
- # Same currency - no conversion needed
90
- if from_curr == to_curr:
91
- return float(amount)
92
-
93
- result = 0.0
94
- conversion_successful = False
95
-
96
- # Try CBR rates first (best for RUB conversions)
97
- if not conversion_successful:
98
- try:
99
- result = self._convert_via_cbr(amount, from_curr, to_curr)
100
- if result > 0:
101
- conversion_successful = True
102
- logger.debug(f"Converted via CBR: {amount} {from_curr} = {result} {to_curr}")
103
- except Exception as e:
104
- logger.debug(f"CBR conversion failed: {e}")
105
-
106
- # Try ECB rates (good for EUR conversions)
107
- if not conversion_successful:
108
- try:
109
- result = self._convert_via_ecb(amount, from_curr, to_curr)
110
- if result > 0:
111
- conversion_successful = True
112
- logger.debug(f"Converted via ECB: {amount} {from_curr} = {result} {to_curr}")
113
- except Exception as e:
114
- logger.debug(f"ECB conversion failed: {e}")
115
-
116
- # Fallback to currency_converter library
117
- if not conversion_successful and self._fallback_converter:
118
- try:
119
- fallback_date = date_obj.date() if isinstance(date_obj, datetime) else date_obj
120
- result = float(self._fallback_converter.convert(
121
- amount, from_curr, to_curr, date=fallback_date
122
- ))
123
- conversion_successful = True
124
- logger.debug(f"Converted via fallback: {amount} {from_curr} = {result} {to_curr}")
125
- except Exception as e:
126
- logger.debug(f"Fallback conversion failed: {e}")
127
-
128
- if not conversion_successful:
129
- raise ValueError(f"Unable to convert {from_curr} to {to_curr}")
130
-
131
- # Apply rounding
132
- if round_to is not None:
133
- result = round(result, round_to)
134
-
135
- return result
136
-
137
- def _convert_via_cbr(self, amount: float, from_curr: str, to_curr: str) -> float:
138
- """Convert using CBR (Central Bank of Russia) rates."""
139
- # Get CBR rates
140
- cbr_rates = self._get_cbr_rates()
141
- if not cbr_rates:
142
- raise ValueError("CBR rates not available")
143
-
144
- # Check if both currencies are available
145
- if from_curr not in cbr_rates or to_curr not in cbr_rates:
146
- raise ValueError(f"Currency pair {from_curr}/{to_curr} not available in CBR rates")
147
-
148
- # Convert via RUB
149
- from_rate = cbr_rates[from_curr] # How many RUB for 1 unit of from_curr
150
- to_rate = cbr_rates[to_curr] # How many RUB for 1 unit of to_curr
151
-
152
- # amount * from_rate = RUB amount
153
- # RUB amount / to_rate = to_curr amount
154
- result = amount * (from_rate / to_rate)
155
- return result
156
-
157
- def _convert_via_ecb(self, amount: float, from_curr: str, to_curr: str) -> float:
158
- """Convert using ECB (European Central Bank) rates."""
159
- # Get ECB rates
160
- ecb_rates = self._get_ecb_rates()
161
- if not ecb_rates:
162
- raise ValueError("ECB rates not available")
163
-
164
- # ECB rates are EUR-based
165
- if from_curr == 'EUR':
166
- if to_curr not in ecb_rates:
167
- raise ValueError(f"Currency {to_curr} not available in ECB rates")
168
- return amount * ecb_rates[to_curr]
169
-
170
- elif to_curr == 'EUR':
171
- if from_curr not in ecb_rates:
172
- raise ValueError(f"Currency {from_curr} not available in ECB rates")
173
- return amount / ecb_rates[from_curr]
174
-
175
- else:
176
- # Convert via EUR
177
- if from_curr not in ecb_rates or to_curr not in ecb_rates:
178
- raise ValueError(f"Currency pair {from_curr}/{to_curr} not available in ECB rates")
179
-
180
- # Convert to EUR first, then to target currency
181
- eur_amount = amount / ecb_rates[from_curr]
182
- result = eur_amount * ecb_rates[to_curr]
183
- return result
184
-
185
- def _get_cbr_rates(self) -> Dict[str, float]:
186
- """Get CBR rates from cache or API."""
187
- # Try cache first
188
- cached_rates = self.cache.get_rates("cbr")
189
- if cached_rates:
190
- self._cbr_rates = cached_rates
191
- return cached_rates
192
-
193
- # Fetch from API
194
- try:
195
- response = requests.get(self.CBR_API_URL, timeout=10)
196
- response.raise_for_status()
197
- data = response.json()
198
-
199
- valutes = data.get("Valute", {})
200
- rates = {}
201
-
202
- # Add RUB as base currency
203
- rates["RUB"] = 1.0
204
-
205
- # Process other currencies
206
- for currency_code, item in valutes.items():
207
- if "Value" in item and "Nominal" in item:
208
- # CBR gives rate as: Nominal units of currency = Value RUB
209
- # We want: 1 unit of currency = X RUB
210
- rate = float(item["Value"]) / float(item["Nominal"])
211
- rates[currency_code] = rate
212
-
213
- # Cache the rates
214
- self.cache.set_rates(rates, "cbr")
215
- self._cbr_rates = rates
216
-
217
- logger.info(f"Fetched {len(rates)} CBR rates")
218
- return rates
219
-
220
- except Exception as e:
221
- logger.error(f"Failed to fetch CBR rates: {e}")
222
- return {}
223
-
224
- def _get_ecb_rates(self) -> Dict[str, float]:
225
- """Get ECB rates from cache or API."""
226
- # Try cache first
227
- cached_rates = self.cache.get_rates("ecb")
228
- if cached_rates:
229
- self._ecb_rates = cached_rates
230
- return cached_rates
231
-
232
- # Fetch from API
233
- try:
234
- response = requests.get(self.ECB_API_URL, timeout=10)
235
- response.raise_for_status()
236
- data = response.json()
237
-
238
- rates = data.get("rates", {})
239
- if rates:
240
- # Add EUR as base currency
241
- rates["EUR"] = 1.0
242
-
243
- # Cache the rates
244
- self.cache.set_rates(rates, "ecb")
245
- self._ecb_rates = rates
246
-
247
- logger.info(f"Fetched {len(rates)} ECB rates")
248
- return rates
249
-
250
- except Exception as e:
251
- logger.error(f"Failed to fetch ECB rates: {e}")
252
-
253
- return {}
254
-
255
- def get_available_currencies(self) -> set:
256
- """Get set of all available currency codes."""
257
- currencies = set()
258
-
259
- # Add CBR currencies
260
- cbr_rates = self._get_cbr_rates()
261
- currencies.update(cbr_rates.keys())
262
-
263
- # Add ECB currencies
264
- ecb_rates = self._get_ecb_rates()
265
- currencies.update(ecb_rates.keys())
266
-
267
- # Add fallback currencies
268
- if self._fallback_converter:
269
- try:
270
- currencies.update(self._fallback_converter.currencies)
271
- except Exception as e:
272
- logger.error(f"Failed to get fallback currencies: {e}")
273
-
274
- return currencies
275
-
276
- def refresh_rates(self) -> bool:
277
- """Force refresh all currency rates."""
278
- success = True
279
-
280
- # Clear cache
281
- self.cache.clear_cache()
282
-
283
- # Refresh CBR rates
284
- try:
285
- cbr_rates = self._get_cbr_rates()
286
- if not cbr_rates:
287
- success = False
288
- except Exception as e:
289
- logger.error(f"Failed to refresh CBR rates: {e}")
290
- success = False
291
-
292
- # Refresh ECB rates
293
- try:
294
- ecb_rates = self._get_ecb_rates()
295
- if not ecb_rates:
296
- success = False
297
- except Exception as e:
298
- logger.error(f"Failed to refresh ECB rates: {e}")
299
- success = False
300
-
301
- logger.info(f"Currency rates refresh {'successful' if success else 'failed'}")
302
- return success
303
-
304
- def get_converter_info(self) -> Dict[str, Any]:
305
- """Get information about converter status."""
306
- return {
307
- "cbr_rates_count": len(self._cbr_rates),
308
- "ecb_rates_count": len(self._ecb_rates),
309
- "fallback_available": self._fallback_converter is not None,
310
- "total_currencies": len(self.get_available_currencies()),
311
- "data_sources": {
312
- "cbr": {
313
- "url": self.CBR_API_URL,
314
- "available": len(self._cbr_rates) > 0
315
- },
316
- "ecb": {
317
- "url": self.ECB_API_URL,
318
- "available": len(self._ecb_rates) > 0
319
- },
320
- "fallback": {
321
- "available": self._fallback_converter is not None
322
- }
323
- }
324
- }
@@ -1,277 +0,0 @@
1
- """
2
- Django Currency Service for django_cfg.
3
-
4
- Auto-configuring currency conversion service that integrates with DjangoConfig.
5
- """
6
-
7
- import logging
8
- from typing import Optional, Dict, Any, Union, List
9
- from datetime import datetime, date
10
- from pathlib import Path
11
-
12
- from django_cfg.modules import BaseCfgModule
13
- from .converter import CurrencyConverter
14
- from .cache import CurrencyCache
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
-
19
- class CurrencyError(Exception):
20
- """Base exception for currency-related errors."""
21
- pass
22
-
23
-
24
- class CurrencyConfigError(CurrencyError):
25
- """Raised when configuration is missing or invalid."""
26
- pass
27
-
28
-
29
- class CurrencyConversionError(CurrencyError):
30
- """Raised when currency conversion fails."""
31
- pass
32
-
33
-
34
- class DjangoCurrency(BaseCfgModule):
35
- """
36
- Currency Service for django_cfg, configured via DjangoConfig.
37
-
38
- Provides currency conversion functionality with automatic configuration
39
- from the main DjangoConfig instance.
40
- """
41
-
42
- def __init__(self):
43
- self._converter = None
44
- self._is_configured = None
45
-
46
- @property
47
- def config(self):
48
- """Get the DjangoConfig instance."""
49
- return self.get_config()
50
-
51
- @property
52
- def is_configured(self) -> bool:
53
- """Check if currency service is properly configured."""
54
- if self._is_configured is None:
55
- try:
56
- # Currency service is always available with fallback
57
- self._is_configured = True
58
- except Exception:
59
- self._is_configured = False
60
-
61
- return self._is_configured
62
-
63
- @property
64
- def converter(self) -> CurrencyConverter:
65
- """Get currency converter instance."""
66
- if self._converter is None:
67
- # Let CurrencyConverter handle its own cache
68
- cache_dir = None
69
- try:
70
- # Only override if explicitly configured
71
- if hasattr(self.config, 'currency_cache_dir'):
72
- cache_dir = Path(self.config.currency_cache_dir)
73
- except Exception:
74
- pass
75
-
76
- self._converter = CurrencyConverter(cache_dir=cache_dir)
77
- return self._converter
78
-
79
- @property
80
- def cache(self) -> CurrencyCache:
81
- """Get currency cache instance from converter."""
82
- return self.converter.cache
83
-
84
-
85
- def convert(
86
- self,
87
- amount: float,
88
- from_currency: str,
89
- to_currency: str,
90
- date_obj: Optional[Union[datetime, date]] = None,
91
- round_to: Optional[int] = 2,
92
- fail_silently: bool = False,
93
- ) -> float:
94
- """
95
- Convert amount from one currency to another.
96
-
97
- Args:
98
- amount: Amount to convert
99
- from_currency: Source currency code (e.g., 'USD')
100
- to_currency: Target currency code (e.g., 'EUR')
101
- date_obj: Optional date for historical rates
102
- round_to: Number of decimal places to round to
103
- fail_silently: Don't raise exceptions on failure
104
-
105
- Returns:
106
- Converted amount
107
-
108
- Raises:
109
- CurrencyConversionError: If conversion fails and fail_silently is False
110
- """
111
- try:
112
- if not self.is_configured:
113
- error_msg = "Currency service is not configured"
114
- logger.error(error_msg)
115
- if not fail_silently:
116
- raise CurrencyConfigError(error_msg)
117
- return 0.0
118
-
119
- result = self.converter.convert(
120
- amount=amount,
121
- from_currency=from_currency,
122
- to_currency=to_currency,
123
- date_obj=date_obj,
124
- round_to=round_to
125
- )
126
-
127
- logger.debug(f"Converted {amount} {from_currency} to {result} {to_currency}")
128
- return result
129
-
130
- except Exception as e:
131
- error_msg = f"Failed to convert {amount} {from_currency} to {to_currency}: {e}"
132
- logger.error(error_msg)
133
- if not fail_silently:
134
- raise CurrencyConversionError(error_msg) from e
135
- return 0.0
136
-
137
- def get_rate(
138
- self,
139
- from_currency: str,
140
- to_currency: str,
141
- date_obj: Optional[Union[datetime, date]] = None,
142
- fail_silently: bool = False,
143
- ) -> float:
144
- """
145
- Get exchange rate between two currencies.
146
-
147
- Args:
148
- from_currency: Source currency code
149
- to_currency: Target currency code
150
- date_obj: Optional date for historical rates
151
- fail_silently: Don't raise exceptions on failure
152
-
153
- Returns:
154
- Exchange rate (1 unit of from_currency = X units of to_currency)
155
- """
156
- return self.convert(
157
- amount=1.0,
158
- from_currency=from_currency,
159
- to_currency=to_currency,
160
- date_obj=date_obj,
161
- fail_silently=fail_silently
162
- )
163
-
164
- def get_available_currencies(self) -> set:
165
- """
166
- Get set of available currency codes.
167
-
168
- Returns:
169
- Set of available currency codes
170
- """
171
- try:
172
- return self.converter.get_available_currencies()
173
- except Exception as e:
174
- logger.error(f"Failed to get available currencies: {e}")
175
- return set()
176
-
177
- def refresh_rates(self, fail_silently: bool = False) -> bool:
178
- """
179
- Force refresh currency rates from external APIs.
180
-
181
- Args:
182
- fail_silently: Don't raise exceptions on failure
183
-
184
- Returns:
185
- True if refresh was successful, False otherwise
186
- """
187
- try:
188
- success = self.converter.refresh_rates()
189
- if success:
190
- logger.info("Currency rates refreshed successfully")
191
- else:
192
- logger.warning("Failed to refresh currency rates")
193
- return success
194
-
195
- except Exception as e:
196
- error_msg = f"Failed to refresh currency rates: {e}"
197
- logger.error(error_msg)
198
- if not fail_silently:
199
- raise CurrencyError(error_msg) from e
200
- return False
201
-
202
- def get_config_info(self) -> Dict[str, Any]:
203
- """Get currency service configuration information."""
204
- try:
205
- cache_info = self.cache.get_cache_info()
206
- converter_info = self.converter.get_converter_info()
207
-
208
- return {
209
- "configured": self.is_configured,
210
- "cache_directory": str(self.cache.cache_dir),
211
- "cache_info": cache_info,
212
- "converter_info": converter_info,
213
- "available_currencies_count": len(self.get_available_currencies()),
214
- }
215
- except Exception as e:
216
- logger.error(f"Failed to get config info: {e}")
217
- return {
218
- "configured": False,
219
- "error": str(e)
220
- }
221
-
222
- def convert_multiple(
223
- self,
224
- amounts: List[float],
225
- from_currencies: List[str],
226
- to_currencies: List[str],
227
- fail_silently: bool = True,
228
- ) -> List[float]:
229
- """
230
- Convert multiple currency amounts in batch.
231
-
232
- Args:
233
- amounts: List of amounts to convert
234
- from_currencies: List of source currency codes
235
- to_currencies: List of target currency codes
236
- fail_silently: Don't raise exceptions on individual failures
237
-
238
- Returns:
239
- List of converted amounts (0.0 for failed conversions)
240
- """
241
- if not (len(amounts) == len(from_currencies) == len(to_currencies)):
242
- raise ValueError("All input lists must have the same length")
243
-
244
- results = []
245
- for amount, from_curr, to_curr in zip(amounts, from_currencies, to_currencies):
246
- try:
247
- result = self.convert(
248
- amount=amount,
249
- from_currency=from_curr,
250
- to_currency=to_curr,
251
- fail_silently=fail_silently
252
- )
253
- results.append(result)
254
- except Exception as e:
255
- logger.error(f"Failed to convert {amount} {from_curr} to {to_curr}: {e}")
256
- results.append(0.0)
257
-
258
- return results
259
-
260
- @classmethod
261
- def send_currency_alert(cls, message: str, rates: Optional[Dict[str, float]] = None) -> None:
262
- """Send currency alert via configured notification services."""
263
- try:
264
- # Try to send via Telegram if available
265
- from django_cfg.modules.django_telegram import DjangoTelegram
266
- telegram = DjangoTelegram()
267
-
268
- text = f"💱 <b>Currency Alert</b>\n\n{message}"
269
- if rates:
270
- text += "\n\n<b>Current Rates:</b>\n"
271
- for pair, rate in rates.items():
272
- text += f"• {pair}: {rate:.4f}\n"
273
-
274
- telegram.send_message(text, parse_mode="HTML", fail_silently=True)
275
-
276
- except Exception as e:
277
- logger.error(f"Failed to send currency alert: {e}")