django-cfg 1.2.23__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
- django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
- django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
- django_cfg/apps/payments/config/__init__.py +15 -37
- django_cfg/apps/payments/config/module.py +30 -122
- django_cfg/apps/payments/config/providers.py +22 -0
- django_cfg/apps/payments/config/settings.py +53 -93
- django_cfg/apps/payments/config/utils.py +10 -156
- django_cfg/apps/payments/management/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/README.md +178 -0
- django_cfg/apps/payments/management/commands/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
- django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
- django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
- django_cfg/apps/payments/managers/currency_manager.py +65 -14
- django_cfg/apps/payments/middleware/api_access.py +33 -0
- django_cfg/apps/payments/migrations/0001_initial.py +94 -1
- django_cfg/apps/payments/models/payments.py +110 -0
- django_cfg/apps/payments/services/__init__.py +7 -1
- django_cfg/apps/payments/services/core/balance_service.py +14 -16
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +212 -29
- django_cfg/apps/payments/services/core/subscription_service.py +15 -17
- django_cfg/apps/payments/services/internal_types.py +31 -0
- django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
- django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
- django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +14 -3
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/registry.py +4 -0
- django_cfg/apps/payments/services/security/__init__.py +34 -0
- django_cfg/apps/payments/services/security/error_handler.py +637 -0
- django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
- django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
- django_cfg/apps/payments/signals/api_key_signals.py +10 -0
- django_cfg/apps/payments/signals/payment_signals.py +3 -2
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/utils/__init__.py +7 -4
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +2 -0
- django_cfg/apps/payments/views/payment_views.py +40 -2
- django_cfg/apps/payments/views/webhook_views.py +266 -0
- django_cfg/apps/payments/viewsets.py +65 -0
- django_cfg/cli/README.md +2 -2
- django_cfg/cli/commands/create_project.py +1 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/main.py +1 -1
- django_cfg/cli/utils.py +5 -5
- django_cfg/core/config.py +18 -4
- django_cfg/models/payments.py +546 -0
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +11 -5
- django_cfg/modules/django_currency/README.md +104 -269
- django_cfg/modules/django_currency/__init__.py +99 -41
- django_cfg/modules/django_currency/clients/__init__.py +11 -0
- django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
- django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
- django_cfg/modules/django_currency/core/__init__.py +42 -0
- django_cfg/modules/django_currency/core/converter.py +169 -0
- django_cfg/modules/django_currency/core/exceptions.py +28 -0
- django_cfg/modules/django_currency/core/models.py +54 -0
- django_cfg/modules/django_currency/database/__init__.py +25 -0
- django_cfg/modules/django_currency/database/database_loader.py +507 -0
- django_cfg/modules/django_currency/utils/__init__.py +9 -0
- django_cfg/modules/django_currency/utils/cache.py +92 -0
- django_cfg/registry/core.py +10 -0
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/METADATA +10 -6
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/RECORD +77 -51
- django_cfg/apps/agents/examples/__init__.py +0 -3
- django_cfg/apps/agents/examples/simple_example.py +0 -161
- django_cfg/apps/knowbase/examples/__init__.py +0 -3
- django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
- django_cfg/modules/django_currency/cache.py +0 -430
- django_cfg/modules/django_currency/converter.py +0 -324
- django_cfg/modules/django_currency/service.py +0 -277
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.23.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}")
|
File without changes
|
File without changes
|
File without changes
|