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,430 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Currency Cache Management for django_currency.
|
3
|
-
|
4
|
-
Handles file-based and memory caching of currency rates with TTL support.
|
5
|
-
Uses YAML format for better readability and human-friendly configuration.
|
6
|
-
"""
|
7
|
-
|
8
|
-
import logging
|
9
|
-
import yaml
|
10
|
-
from datetime import datetime, timedelta
|
11
|
-
from pathlib import Path
|
12
|
-
from typing import Dict, Optional, Any
|
13
|
-
from cachetools import TTLCache
|
14
|
-
|
15
|
-
logger = logging.getLogger(__name__)
|
16
|
-
|
17
|
-
|
18
|
-
class CurrencyCache:
|
19
|
-
"""
|
20
|
-
Currency cache manager with file and memory caching.
|
21
|
-
|
22
|
-
Features:
|
23
|
-
- TTL-based memory cache (24 hours default)
|
24
|
-
- File-based persistent cache
|
25
|
-
- Automatic cache invalidation
|
26
|
-
- Thread-safe operations
|
27
|
-
"""
|
28
|
-
|
29
|
-
DEFAULT_TTL = 86400 # 24 hours in seconds
|
30
|
-
DEFAULT_CACHE_SIZE = 1000
|
31
|
-
CACHE_FILENAME = "currency_rates.yaml"
|
32
|
-
|
33
|
-
def __init__(
|
34
|
-
self,
|
35
|
-
cache_dir: Optional[Path] = None,
|
36
|
-
ttl: int = DEFAULT_TTL,
|
37
|
-
max_size: int = DEFAULT_CACHE_SIZE
|
38
|
-
):
|
39
|
-
"""
|
40
|
-
Initialize currency cache.
|
41
|
-
|
42
|
-
Args:
|
43
|
-
cache_dir: Directory for file cache
|
44
|
-
ttl: Time-to-live for memory cache in seconds
|
45
|
-
max_size: Maximum number of items in memory cache
|
46
|
-
"""
|
47
|
-
# Default cache directory inside django-cfg structure
|
48
|
-
if cache_dir is None:
|
49
|
-
def make_cache_dir(cache_dir=Path.cwd()):
|
50
|
-
return Path(cache_dir) / ".cache" / "currency"
|
51
|
-
default_cache_dir = make_cache_dir()
|
52
|
-
|
53
|
-
try:
|
54
|
-
from django.conf import settings
|
55
|
-
# Check if Django is configured before accessing settings
|
56
|
-
if settings.configured and hasattr(settings, 'BASE_DIR'):
|
57
|
-
default_cache_dir = make_cache_dir(settings.BASE_DIR)
|
58
|
-
except (ImportError, Exception):
|
59
|
-
pass
|
60
|
-
else:
|
61
|
-
default_cache_dir = Path(cache_dir)
|
62
|
-
|
63
|
-
self.cache_dir = default_cache_dir
|
64
|
-
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
65
|
-
self.cache_file = self.cache_dir / self.CACHE_FILENAME
|
66
|
-
|
67
|
-
# TTL Cache for in-memory storage
|
68
|
-
self._memory_cache = TTLCache(maxsize=max_size, ttl=ttl)
|
69
|
-
|
70
|
-
# File cache metadata
|
71
|
-
self._file_cache_data = None
|
72
|
-
self._file_cache_timestamp = None
|
73
|
-
|
74
|
-
logger.info(f"Currency cache initialized: {self.cache_dir}")
|
75
|
-
|
76
|
-
def get_rates(self, source: str = "cbr") -> Optional[Dict[str, float]]:
|
77
|
-
"""
|
78
|
-
Get currency rates from cache.
|
79
|
-
|
80
|
-
Args:
|
81
|
-
source: Rate source identifier (e.g., 'cbr', 'ecb')
|
82
|
-
|
83
|
-
Returns:
|
84
|
-
Dictionary of currency rates or None if not cached
|
85
|
-
"""
|
86
|
-
cache_key = f"rates_{source}"
|
87
|
-
|
88
|
-
# Try memory cache first
|
89
|
-
if cache_key in self._memory_cache:
|
90
|
-
logger.debug(f"Retrieved rates from memory cache: {source}")
|
91
|
-
return self._memory_cache[cache_key]
|
92
|
-
|
93
|
-
# Try file cache
|
94
|
-
file_rates = self._load_from_file(source)
|
95
|
-
if file_rates:
|
96
|
-
# Update memory cache
|
97
|
-
self._memory_cache[cache_key] = file_rates
|
98
|
-
logger.debug(f"Retrieved rates from file cache: {source}")
|
99
|
-
return file_rates
|
100
|
-
|
101
|
-
logger.debug(f"No cached rates found for source: {source}")
|
102
|
-
return None
|
103
|
-
|
104
|
-
def set_rates(
|
105
|
-
self,
|
106
|
-
rates: Dict[str, float],
|
107
|
-
source: str = "cbr",
|
108
|
-
save_to_file: bool = True
|
109
|
-
) -> bool:
|
110
|
-
"""
|
111
|
-
Store currency rates in cache.
|
112
|
-
|
113
|
-
Args:
|
114
|
-
rates: Dictionary of currency rates
|
115
|
-
source: Rate source identifier
|
116
|
-
save_to_file: Whether to persist to file
|
117
|
-
|
118
|
-
Returns:
|
119
|
-
True if successfully cached, False otherwise
|
120
|
-
"""
|
121
|
-
try:
|
122
|
-
cache_key = f"rates_{source}"
|
123
|
-
|
124
|
-
# Store in memory cache
|
125
|
-
self._memory_cache[cache_key] = rates
|
126
|
-
|
127
|
-
# Store in file cache if requested
|
128
|
-
if save_to_file:
|
129
|
-
self._save_to_file(rates, source)
|
130
|
-
|
131
|
-
logger.info(f"Cached {len(rates)} rates for source: {source}")
|
132
|
-
return True
|
133
|
-
|
134
|
-
except Exception as e:
|
135
|
-
logger.error(f"Failed to cache rates for {source}: {e}")
|
136
|
-
return False
|
137
|
-
|
138
|
-
def _load_from_file(self, source: str) -> Optional[Dict[str, float]]:
|
139
|
-
"""Load rates from file cache."""
|
140
|
-
try:
|
141
|
-
if not self.cache_file.exists():
|
142
|
-
return None
|
143
|
-
|
144
|
-
with open(self.cache_file, 'r', encoding='utf-8') as f:
|
145
|
-
data = yaml.safe_load(f)
|
146
|
-
|
147
|
-
# Check if data is for the requested source
|
148
|
-
if data.get('source') != source:
|
149
|
-
return None
|
150
|
-
|
151
|
-
# Check if data is still valid (not expired)
|
152
|
-
timestamp_str = data.get('timestamp')
|
153
|
-
if timestamp_str:
|
154
|
-
timestamp = datetime.fromisoformat(timestamp_str)
|
155
|
-
if datetime.now() - timestamp > timedelta(seconds=self.DEFAULT_TTL):
|
156
|
-
logger.debug(f"File cache expired for source: {source}")
|
157
|
-
return None
|
158
|
-
|
159
|
-
rates = data.get('rates', {})
|
160
|
-
if rates:
|
161
|
-
self._file_cache_data = data
|
162
|
-
self._file_cache_timestamp = timestamp if timestamp_str else None
|
163
|
-
return rates
|
164
|
-
|
165
|
-
except Exception as e:
|
166
|
-
logger.error(f"Failed to load file cache for {source}: {e}")
|
167
|
-
|
168
|
-
return None
|
169
|
-
|
170
|
-
def _save_to_file(self, rates: Dict[str, float], source: str) -> bool:
|
171
|
-
"""Save rates to file cache."""
|
172
|
-
try:
|
173
|
-
now = datetime.now()
|
174
|
-
data = {
|
175
|
-
'source': source,
|
176
|
-
'timestamp': now.isoformat(),
|
177
|
-
'rates': rates,
|
178
|
-
'metadata': {
|
179
|
-
'count': len(rates),
|
180
|
-
'cache_version': '1.0',
|
181
|
-
'format': 'YAML',
|
182
|
-
'description': f'Currency rates from {source.upper()} API',
|
183
|
-
'updated_at': now.strftime('%Y-%m-%d %H:%M:%S UTC'),
|
184
|
-
'ttl_hours': self.DEFAULT_TTL // 3600,
|
185
|
-
'next_update': (now + timedelta(seconds=self.DEFAULT_TTL)).strftime('%Y-%m-%d %H:%M:%S UTC')
|
186
|
-
}
|
187
|
-
}
|
188
|
-
|
189
|
-
# Atomic write using temporary file
|
190
|
-
temp_file = self.cache_file.with_suffix('.tmp')
|
191
|
-
with open(temp_file, 'w', encoding='utf-8') as f:
|
192
|
-
# Write header comment
|
193
|
-
f.write(f"# Currency Rates Cache - Django CFG\n")
|
194
|
-
f.write(f"# Generated: {now.strftime('%Y-%m-%d %H:%M:%S UTC')}\n")
|
195
|
-
f.write(f"# Source: {source.upper()} API\n")
|
196
|
-
f.write(f"# Rates count: {len(rates)}\n")
|
197
|
-
f.write(f"# TTL: {self.DEFAULT_TTL // 3600} hours\n")
|
198
|
-
f.write(f"# Auto-generated - do not edit manually\n\n")
|
199
|
-
|
200
|
-
yaml.safe_dump(
|
201
|
-
data,
|
202
|
-
f,
|
203
|
-
default_flow_style=False,
|
204
|
-
allow_unicode=True,
|
205
|
-
sort_keys=False,
|
206
|
-
indent=2,
|
207
|
-
width=120
|
208
|
-
)
|
209
|
-
|
210
|
-
# Move temp file to final location
|
211
|
-
temp_file.replace(self.cache_file)
|
212
|
-
|
213
|
-
self._file_cache_data = data
|
214
|
-
self._file_cache_timestamp = datetime.now()
|
215
|
-
|
216
|
-
logger.debug(f"Saved {len(rates)} rates to file cache: {source}")
|
217
|
-
return True
|
218
|
-
|
219
|
-
except Exception as e:
|
220
|
-
logger.error(f"Failed to save file cache for {source}: {e}")
|
221
|
-
return False
|
222
|
-
|
223
|
-
def clear_cache(self, source: Optional[str] = None) -> bool:
|
224
|
-
"""
|
225
|
-
Clear cache for specific source or all sources.
|
226
|
-
|
227
|
-
Args:
|
228
|
-
source: Source to clear (None for all)
|
229
|
-
|
230
|
-
Returns:
|
231
|
-
True if successfully cleared
|
232
|
-
"""
|
233
|
-
try:
|
234
|
-
if source:
|
235
|
-
# Clear specific source from memory
|
236
|
-
cache_key = f"rates_{source}"
|
237
|
-
self._memory_cache.pop(cache_key, None)
|
238
|
-
|
239
|
-
# Clear file cache if it matches the source
|
240
|
-
if (self._file_cache_data and
|
241
|
-
self._file_cache_data.get('source') == source):
|
242
|
-
if self.cache_file.exists():
|
243
|
-
self.cache_file.unlink()
|
244
|
-
self._file_cache_data = None
|
245
|
-
self._file_cache_timestamp = None
|
246
|
-
|
247
|
-
logger.info(f"Cleared cache for source: {source}")
|
248
|
-
else:
|
249
|
-
# Clear all caches
|
250
|
-
self._memory_cache.clear()
|
251
|
-
if self.cache_file.exists():
|
252
|
-
self.cache_file.unlink()
|
253
|
-
self._file_cache_data = None
|
254
|
-
self._file_cache_timestamp = None
|
255
|
-
|
256
|
-
logger.info("Cleared all currency caches")
|
257
|
-
|
258
|
-
return True
|
259
|
-
|
260
|
-
except Exception as e:
|
261
|
-
logger.error(f"Failed to clear cache: {e}")
|
262
|
-
return False
|
263
|
-
|
264
|
-
def get_cache_info(self) -> Dict[str, Any]:
|
265
|
-
"""Get information about cache status."""
|
266
|
-
try:
|
267
|
-
memory_info = {
|
268
|
-
'size': len(self._memory_cache),
|
269
|
-
'max_size': self._memory_cache.maxsize,
|
270
|
-
'ttl': self._memory_cache.ttl,
|
271
|
-
'keys': list(self._memory_cache.keys())
|
272
|
-
}
|
273
|
-
|
274
|
-
file_info = {
|
275
|
-
'exists': self.cache_file.exists(),
|
276
|
-
'path': str(self.cache_file),
|
277
|
-
'size_bytes': self.cache_file.stat().st_size if self.cache_file.exists() else 0,
|
278
|
-
}
|
279
|
-
|
280
|
-
if self._file_cache_data:
|
281
|
-
file_info.update({
|
282
|
-
'source': self._file_cache_data.get('source'),
|
283
|
-
'timestamp': self._file_cache_data.get('timestamp'),
|
284
|
-
'rates_count': len(self._file_cache_data.get('rates', {}))
|
285
|
-
})
|
286
|
-
|
287
|
-
return {
|
288
|
-
'cache_directory': str(self.cache_dir),
|
289
|
-
'memory_cache': memory_info,
|
290
|
-
'file_cache': file_info,
|
291
|
-
'status': 'active'
|
292
|
-
}
|
293
|
-
|
294
|
-
except Exception as e:
|
295
|
-
logger.error(f"Failed to get cache info: {e}")
|
296
|
-
return {
|
297
|
-
'status': 'error',
|
298
|
-
'error': str(e)
|
299
|
-
}
|
300
|
-
|
301
|
-
def is_cache_valid(self, source: str = "cbr") -> bool:
|
302
|
-
"""Check if cache is valid and not expired."""
|
303
|
-
cache_key = f"rates_{source}"
|
304
|
-
|
305
|
-
# Check memory cache
|
306
|
-
if cache_key in self._memory_cache:
|
307
|
-
return True
|
308
|
-
|
309
|
-
# Check file cache
|
310
|
-
if self._file_cache_timestamp:
|
311
|
-
age = datetime.now() - self._file_cache_timestamp
|
312
|
-
return age.total_seconds() < self.DEFAULT_TTL
|
313
|
-
|
314
|
-
return False
|
315
|
-
|
316
|
-
def get_cache_age(self, source: str = "cbr") -> Optional[timedelta]:
|
317
|
-
"""Get age of cached data."""
|
318
|
-
if self._file_cache_timestamp:
|
319
|
-
return datetime.now() - self._file_cache_timestamp
|
320
|
-
return None
|
321
|
-
|
322
|
-
def _get_currency_description(self, currency_code: str) -> str:
|
323
|
-
"""Get human-readable description for currency code."""
|
324
|
-
currency_names = {
|
325
|
-
'USD': 'US Dollar',
|
326
|
-
'EUR': 'Euro',
|
327
|
-
'GBP': 'British Pound',
|
328
|
-
'JPY': 'Japanese Yen',
|
329
|
-
'CNY': 'Chinese Yuan',
|
330
|
-
'KRW': 'South Korean Won',
|
331
|
-
'RUB': 'Russian Ruble',
|
332
|
-
'CAD': 'Canadian Dollar',
|
333
|
-
'AUD': 'Australian Dollar',
|
334
|
-
'CHF': 'Swiss Franc',
|
335
|
-
'SEK': 'Swedish Krona',
|
336
|
-
'NOK': 'Norwegian Krone',
|
337
|
-
'DKK': 'Danish Krone',
|
338
|
-
'PLN': 'Polish Zloty',
|
339
|
-
'CZK': 'Czech Koruna',
|
340
|
-
'HUF': 'Hungarian Forint',
|
341
|
-
'TRY': 'Turkish Lira',
|
342
|
-
'BRL': 'Brazilian Real',
|
343
|
-
'MXN': 'Mexican Peso',
|
344
|
-
'INR': 'Indian Rupee',
|
345
|
-
'SGD': 'Singapore Dollar',
|
346
|
-
'HKD': 'Hong Kong Dollar',
|
347
|
-
'NZD': 'New Zealand Dollar',
|
348
|
-
'ZAR': 'South African Rand',
|
349
|
-
'THB': 'Thai Baht',
|
350
|
-
'MYR': 'Malaysian Ringgit',
|
351
|
-
'PHP': 'Philippine Peso',
|
352
|
-
'IDR': 'Indonesian Rupiah',
|
353
|
-
'VND': 'Vietnamese Dong',
|
354
|
-
}
|
355
|
-
return currency_names.get(currency_code, currency_code)
|
356
|
-
|
357
|
-
def export_rates_yaml(self, source: str = "cbr", output_file: Optional[Path] = None) -> str:
|
358
|
-
"""
|
359
|
-
Export rates to a formatted YAML file with comments.
|
360
|
-
|
361
|
-
Args:
|
362
|
-
source: Rate source to export
|
363
|
-
output_file: Optional output file path
|
364
|
-
|
365
|
-
Returns:
|
366
|
-
YAML content as string
|
367
|
-
"""
|
368
|
-
rates = self.get_rates(source)
|
369
|
-
if not rates:
|
370
|
-
return "# No rates available for export"
|
371
|
-
|
372
|
-
now = datetime.now()
|
373
|
-
|
374
|
-
# Create structured data with comments
|
375
|
-
yaml_content = f"""# Currency Exchange Rates - Django CFG
|
376
|
-
# Source: {source.upper()} API
|
377
|
-
# Generated: {now.strftime('%Y-%m-%d %H:%M:%S UTC')}
|
378
|
-
# Total currencies: {len(rates)}
|
379
|
-
# Cache TTL: {self.DEFAULT_TTL // 3600} hours
|
380
|
-
#
|
381
|
-
# Format: currency_code: rate_to_base_currency
|
382
|
-
# Base currency for CBR: RUB (Russian Ruble)
|
383
|
-
# Base currency for ECB: EUR (Euro)
|
384
|
-
|
385
|
-
source: {source}
|
386
|
-
timestamp: {now.isoformat()}
|
387
|
-
|
388
|
-
metadata:
|
389
|
-
count: {len(rates)}
|
390
|
-
cache_version: '1.0'
|
391
|
-
format: 'YAML'
|
392
|
-
description: 'Currency rates from {source.upper()} API'
|
393
|
-
updated_at: '{now.strftime('%Y-%m-%d %H:%M:%S UTC')}'
|
394
|
-
ttl_hours: {self.DEFAULT_TTL // 3600}
|
395
|
-
next_update: '{(now + timedelta(seconds=self.DEFAULT_TTL)).strftime('%Y-%m-%d %H:%M:%S UTC')}'
|
396
|
-
|
397
|
-
# Currency Rates
|
398
|
-
rates:
|
399
|
-
"""
|
400
|
-
|
401
|
-
# Sort currencies: major currencies first, then alphabetically
|
402
|
-
major_currencies = ['USD', 'EUR', 'GBP', 'JPY', 'CNY', 'KRW', 'RUB', 'CAD', 'AUD', 'CHF']
|
403
|
-
sorted_currencies = []
|
404
|
-
|
405
|
-
# Add major currencies first (if they exist)
|
406
|
-
for curr in major_currencies:
|
407
|
-
if curr in rates:
|
408
|
-
sorted_currencies.append(curr)
|
409
|
-
|
410
|
-
# Add remaining currencies alphabetically
|
411
|
-
remaining = sorted([curr for curr in rates.keys() if curr not in major_currencies])
|
412
|
-
sorted_currencies.extend(remaining)
|
413
|
-
|
414
|
-
# Add rates with comments
|
415
|
-
for currency in sorted_currencies:
|
416
|
-
rate = rates[currency]
|
417
|
-
description = self._get_currency_description(currency)
|
418
|
-
yaml_content += f" {currency}: {rate:<12.6f} # {description}\n"
|
419
|
-
|
420
|
-
# Save to file if requested
|
421
|
-
if output_file:
|
422
|
-
try:
|
423
|
-
output_file.parent.mkdir(parents=True, exist_ok=True)
|
424
|
-
with open(output_file, 'w', encoding='utf-8') as f:
|
425
|
-
f.write(yaml_content)
|
426
|
-
logger.info(f"Exported rates to: {output_file}")
|
427
|
-
except Exception as e:
|
428
|
-
logger.error(f"Failed to export rates to file: {e}")
|
429
|
-
|
430
|
-
return yaml_content
|