django-cfg 1.2.29__py3-none-any.whl → 1.2.31__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/payments/admin/__init__.py +3 -2
- django_cfg/apps/payments/admin/balance_admin.py +18 -18
- django_cfg/apps/payments/admin/currencies_admin.py +319 -131
- django_cfg/apps/payments/admin/payments_admin.py +15 -4
- django_cfg/apps/payments/config/module.py +2 -2
- django_cfg/apps/payments/config/utils.py +2 -2
- django_cfg/apps/payments/decorators.py +2 -2
- django_cfg/apps/payments/management/commands/README.md +95 -127
- django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
- django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
- django_cfg/apps/payments/managers/__init__.py +3 -2
- django_cfg/apps/payments/managers/balance_manager.py +2 -2
- django_cfg/apps/payments/managers/currency_manager.py +272 -49
- django_cfg/apps/payments/managers/payment_manager.py +161 -13
- django_cfg/apps/payments/middleware/api_access.py +2 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
- django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
- django_cfg/apps/payments/models/__init__.py +3 -2
- django_cfg/apps/payments/models/currencies.py +187 -71
- django_cfg/apps/payments/models/payments.py +3 -2
- django_cfg/apps/payments/serializers/__init__.py +3 -2
- django_cfg/apps/payments/serializers/currencies.py +20 -12
- django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
- django_cfg/apps/payments/services/core/balance_service.py +2 -2
- django_cfg/apps/payments/services/core/fallback_service.py +2 -2
- django_cfg/apps/payments/services/core/payment_service.py +3 -6
- django_cfg/apps/payments/services/core/subscription_service.py +4 -7
- django_cfg/apps/payments/services/internal_types.py +171 -7
- django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
- django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
- django_cfg/apps/payments/services/providers/base.py +144 -43
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
- django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
- django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
- django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
- django_cfg/apps/payments/services/providers/registry.py +294 -11
- django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
- django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
- django_cfg/apps/payments/services/security/error_handler.py +6 -8
- django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
- django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
- django_cfg/apps/payments/signals/api_key_signals.py +2 -2
- django_cfg/apps/payments/signals/payment_signals.py +11 -5
- django_cfg/apps/payments/signals/subscription_signals.py +2 -2
- django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
- django_cfg/apps/payments/templates/payments/base.html +4 -4
- django_cfg/apps/payments/templates/payments/components/payment_card.html +6 -6
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +4 -4
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +14 -7
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +2 -2
- django_cfg/apps/payments/templates/payments/components/status_badge.html +8 -1
- django_cfg/apps/payments/templates/payments/components/status_overview.html +34 -30
- django_cfg/apps/payments/templates/payments/dashboard.html +202 -290
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
- django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
- django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
- django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
- django_cfg/apps/payments/templates/payments/stats.html +261 -0
- django_cfg/apps/payments/templates/payments/test.html +213 -0
- django_cfg/apps/payments/urls.py +3 -1
- django_cfg/apps/payments/{urls_templates.py → urls_admin.py} +6 -0
- django_cfg/apps/payments/utils/__init__.py +1 -3
- django_cfg/apps/payments/utils/billing_utils.py +2 -2
- django_cfg/apps/payments/utils/config_utils.py +2 -8
- django_cfg/apps/payments/utils/validation_utils.py +2 -2
- django_cfg/apps/payments/views/__init__.py +3 -2
- django_cfg/apps/payments/views/currency_views.py +31 -20
- django_cfg/apps/payments/views/payment_views.py +2 -2
- django_cfg/apps/payments/views/templates/ajax.py +141 -2
- django_cfg/apps/payments/views/templates/base.py +21 -13
- django_cfg/apps/payments/views/templates/payment_detail.py +1 -1
- django_cfg/apps/payments/views/templates/payment_management.py +34 -40
- django_cfg/apps/payments/views/templates/stats.py +8 -4
- django_cfg/apps/payments/views/webhook_views.py +2 -2
- django_cfg/apps/payments/viewsets.py +3 -2
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +4 -4
- django_cfg/core/config.py +35 -0
- django_cfg/models/payments.py +2 -8
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_unfold/dashboard.py +7 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/METADATA +2 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/RECORD +118 -96
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -1,246 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Management command to populate initial currency data.
|
3
|
-
|
4
|
-
This is a simpler version of update_currencies designed for initial setup.
|
5
|
-
Use this when you need to populate an empty currency database.
|
6
|
-
|
7
|
-
Usage:
|
8
|
-
python manage.py populate_currencies
|
9
|
-
python manage.py populate_currencies --quick
|
10
|
-
python manage.py populate_currencies --crypto-only
|
11
|
-
python manage.py populate_currencies --fiat-only
|
12
|
-
"""
|
13
|
-
|
14
|
-
import logging
|
15
|
-
from typing import List
|
16
|
-
|
17
|
-
from django.core.management.base import BaseCommand, CommandError
|
18
|
-
from django.db import transaction
|
19
|
-
from django.utils import timezone
|
20
|
-
|
21
|
-
from django_cfg.modules.django_currency.database.database_loader import (
|
22
|
-
create_database_loader,
|
23
|
-
DatabaseLoaderConfig
|
24
|
-
)
|
25
|
-
from django_cfg.apps.payments.models.currencies import Currency
|
26
|
-
|
27
|
-
logger = logging.getLogger(__name__)
|
28
|
-
|
29
|
-
|
30
|
-
class Command(BaseCommand):
|
31
|
-
"""
|
32
|
-
Simple command to populate initial currency data.
|
33
|
-
|
34
|
-
Optimized for first-time setup with sensible defaults.
|
35
|
-
"""
|
36
|
-
|
37
|
-
help = 'Populate initial currency data (for empty database)'
|
38
|
-
|
39
|
-
def add_arguments(self, parser):
|
40
|
-
"""Add command line arguments."""
|
41
|
-
parser.add_argument(
|
42
|
-
'--quick',
|
43
|
-
action='store_true',
|
44
|
-
help='Quick setup with top 50 cryptocurrencies and 20 fiat currencies'
|
45
|
-
)
|
46
|
-
|
47
|
-
parser.add_argument(
|
48
|
-
'--crypto-only',
|
49
|
-
action='store_true',
|
50
|
-
help='Only populate cryptocurrencies'
|
51
|
-
)
|
52
|
-
|
53
|
-
parser.add_argument(
|
54
|
-
'--fiat-only',
|
55
|
-
action='store_true',
|
56
|
-
help='Only populate fiat currencies'
|
57
|
-
)
|
58
|
-
|
59
|
-
parser.add_argument(
|
60
|
-
'--skip-existing',
|
61
|
-
action='store_true',
|
62
|
-
help='Skip currencies that already exist in database'
|
63
|
-
)
|
64
|
-
|
65
|
-
def handle(self, *args, **options):
|
66
|
-
"""Main command handler."""
|
67
|
-
|
68
|
-
self.stdout.write(
|
69
|
-
self.style.SUCCESS('🪙 Populating currency database...')
|
70
|
-
)
|
71
|
-
|
72
|
-
# Check if database is empty
|
73
|
-
existing_count = Currency.objects.count()
|
74
|
-
if existing_count > 0 and not options['skip_existing']:
|
75
|
-
self.stdout.write(
|
76
|
-
self.style.WARNING(f'⚠️ Database already contains {existing_count} currencies')
|
77
|
-
)
|
78
|
-
response = input('Continue anyway? [y/N]: ')
|
79
|
-
if response.lower() != 'y':
|
80
|
-
self.stdout.write('Cancelled by user')
|
81
|
-
return
|
82
|
-
|
83
|
-
try:
|
84
|
-
# Configure loader based on options
|
85
|
-
if options['quick']:
|
86
|
-
config = DatabaseLoaderConfig(
|
87
|
-
max_cryptocurrencies=50,
|
88
|
-
max_fiat_currencies=20,
|
89
|
-
min_market_cap_usd=10_000_000, # Top coins only
|
90
|
-
coingecko_delay=1.0, # Faster for initial setup
|
91
|
-
)
|
92
|
-
self.stdout.write("⚡ Quick setup mode: top 50 crypto + 20 fiat")
|
93
|
-
else:
|
94
|
-
config = DatabaseLoaderConfig(
|
95
|
-
max_cryptocurrencies=200,
|
96
|
-
max_fiat_currencies=30,
|
97
|
-
min_market_cap_usd=1_000_000,
|
98
|
-
coingecko_delay=1.5,
|
99
|
-
)
|
100
|
-
self.stdout.write("📈 Standard setup: top 200 crypto + 30 fiat")
|
101
|
-
|
102
|
-
loader = create_database_loader(
|
103
|
-
max_cryptocurrencies=config.max_cryptocurrencies,
|
104
|
-
max_fiat_currencies=config.max_fiat_currencies,
|
105
|
-
min_market_cap_usd=config.min_market_cap_usd,
|
106
|
-
coingecko_delay=config.coingecko_delay
|
107
|
-
)
|
108
|
-
|
109
|
-
# Get statistics
|
110
|
-
stats = loader.get_statistics()
|
111
|
-
self.stdout.write(f"📊 Available: {stats['total_currencies']} currencies")
|
112
|
-
|
113
|
-
# Load currency data
|
114
|
-
self.stdout.write("🌐 Fetching currency data from APIs...")
|
115
|
-
fresh_currencies = loader.build_currency_database_data()
|
116
|
-
|
117
|
-
# Filter by type if requested
|
118
|
-
if options['crypto_only']:
|
119
|
-
fresh_currencies = [c for c in fresh_currencies if c.currency_type == 'crypto']
|
120
|
-
self.stdout.write(f"🔗 Crypto only: {len(fresh_currencies)} cryptocurrencies")
|
121
|
-
elif options['fiat_only']:
|
122
|
-
fresh_currencies = [c for c in fresh_currencies if c.currency_type == 'fiat']
|
123
|
-
self.stdout.write(f"💵 Fiat only: {len(fresh_currencies)} fiat currencies")
|
124
|
-
else:
|
125
|
-
crypto_count = sum(1 for c in fresh_currencies if c.currency_type == 'crypto')
|
126
|
-
fiat_count = sum(1 for c in fresh_currencies if c.currency_type == 'fiat')
|
127
|
-
self.stdout.write(f"💰 Mixed: {crypto_count} crypto + {fiat_count} fiat")
|
128
|
-
|
129
|
-
# Populate database
|
130
|
-
self._populate_database(fresh_currencies, options)
|
131
|
-
|
132
|
-
except KeyboardInterrupt:
|
133
|
-
self.stdout.write(
|
134
|
-
self.style.WARNING('\n⚠️ Population interrupted by user')
|
135
|
-
)
|
136
|
-
raise CommandError("Population cancelled")
|
137
|
-
|
138
|
-
except Exception as e:
|
139
|
-
logger.exception("Currency population failed")
|
140
|
-
self.stdout.write(
|
141
|
-
self.style.ERROR(f'❌ Population failed: {str(e)}')
|
142
|
-
)
|
143
|
-
raise CommandError(f"Population failed: {str(e)}")
|
144
|
-
|
145
|
-
def _populate_database(self, currencies: List, options: dict):
|
146
|
-
"""Populate the database with currencies."""
|
147
|
-
|
148
|
-
created_count = 0
|
149
|
-
updated_count = 0
|
150
|
-
skipped_count = 0
|
151
|
-
|
152
|
-
try:
|
153
|
-
with transaction.atomic():
|
154
|
-
self.stdout.write("💾 Populating database...")
|
155
|
-
|
156
|
-
for i, currency_data in enumerate(currencies):
|
157
|
-
try:
|
158
|
-
# Check if exists and should skip
|
159
|
-
if options['skip_existing']:
|
160
|
-
if Currency.objects.filter(code=currency_data.code).exists():
|
161
|
-
skipped_count += 1
|
162
|
-
continue
|
163
|
-
|
164
|
-
# Create or update
|
165
|
-
currency, created = Currency.objects.update_or_create(
|
166
|
-
code=currency_data.code,
|
167
|
-
defaults={
|
168
|
-
'name': currency_data.name,
|
169
|
-
'symbol': currency_data.symbol,
|
170
|
-
'currency_type': currency_data.currency_type,
|
171
|
-
'decimal_places': currency_data.decimal_places,
|
172
|
-
'usd_rate': currency_data.usd_rate,
|
173
|
-
'min_payment_amount': currency_data.min_payment_amount,
|
174
|
-
'is_active': currency_data.is_active,
|
175
|
-
'rate_updated_at': timezone.now()
|
176
|
-
}
|
177
|
-
)
|
178
|
-
|
179
|
-
if created:
|
180
|
-
created_count += 1
|
181
|
-
else:
|
182
|
-
updated_count += 1
|
183
|
-
|
184
|
-
# Progress indicator every 25 currencies
|
185
|
-
if (i + 1) % 25 == 0:
|
186
|
-
self.stdout.write(f" 📊 Progress: {i + 1}/{len(currencies)}")
|
187
|
-
|
188
|
-
except Exception as e:
|
189
|
-
self.stdout.write(
|
190
|
-
self.style.WARNING(f'⚠️ Failed to create {currency_data.code}: {e}')
|
191
|
-
)
|
192
|
-
continue
|
193
|
-
|
194
|
-
# Final summary
|
195
|
-
total_processed = created_count + updated_count
|
196
|
-
self.stdout.write(f"\n🎉 Population completed!")
|
197
|
-
self.stdout.write(f" ✅ Created: {created_count} currencies")
|
198
|
-
self.stdout.write(f" 🔄 Updated: {updated_count} currencies")
|
199
|
-
self.stdout.write(f" ⏭️ Skipped: {skipped_count} currencies")
|
200
|
-
self.stdout.write(f" 📊 Total: {total_processed} currencies processed")
|
201
|
-
|
202
|
-
# Show some examples
|
203
|
-
self._show_examples()
|
204
|
-
|
205
|
-
self.stdout.write(
|
206
|
-
self.style.SUCCESS('\n✅ Currency database is ready for payments!')
|
207
|
-
)
|
208
|
-
|
209
|
-
except Exception as e:
|
210
|
-
self.stdout.write(
|
211
|
-
self.style.ERROR(f'❌ Population failed and rolled back: {str(e)}')
|
212
|
-
)
|
213
|
-
raise
|
214
|
-
|
215
|
-
def _show_examples(self):
|
216
|
-
"""Show some example currencies that were created."""
|
217
|
-
|
218
|
-
# Show top cryptocurrencies
|
219
|
-
crypto_examples = Currency.objects.filter(
|
220
|
-
currency_type=Currency.CurrencyType.CRYPTO
|
221
|
-
).order_by('-usd_rate')[:3]
|
222
|
-
|
223
|
-
if crypto_examples:
|
224
|
-
self.stdout.write(f"\n🔗 Top cryptocurrencies:")
|
225
|
-
for currency in crypto_examples:
|
226
|
-
self.stdout.write(f" • {currency.code}: ${currency.usd_rate:.2f}")
|
227
|
-
|
228
|
-
# Show fiat currencies
|
229
|
-
fiat_examples = Currency.objects.filter(
|
230
|
-
currency_type=Currency.CurrencyType.FIAT
|
231
|
-
).order_by('code')[:5]
|
232
|
-
|
233
|
-
if fiat_examples:
|
234
|
-
self.stdout.write(f"\n💵 Fiat currencies:")
|
235
|
-
for currency in fiat_examples:
|
236
|
-
self.stdout.write(f" • {currency.code}: {currency.name} ({currency.symbol})")
|
237
|
-
|
238
|
-
# Total counts
|
239
|
-
total = Currency.objects.count()
|
240
|
-
crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
|
241
|
-
fiat_count = Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count()
|
242
|
-
|
243
|
-
self.stdout.write(f"\n📊 Database now contains:")
|
244
|
-
self.stdout.write(f" • Total: {total} currencies")
|
245
|
-
self.stdout.write(f" • Crypto: {crypto_count} currencies")
|
246
|
-
self.stdout.write(f" • Fiat: {fiat_count} currencies")
|
@@ -1,336 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Management command to update currency data using django_currency database loader.
|
3
|
-
|
4
|
-
This command automatically populates and updates the payments Currency model
|
5
|
-
with fresh data from external APIs (CoinGecko, YFinance).
|
6
|
-
|
7
|
-
Usage:
|
8
|
-
python manage.py update_currencies
|
9
|
-
python manage.py update_currencies --max-crypto 100 --max-fiat 20
|
10
|
-
python manage.py update_currencies --dry-run
|
11
|
-
python manage.py update_currencies --force-update
|
12
|
-
"""
|
13
|
-
|
14
|
-
import logging
|
15
|
-
from typing import Dict, List, Optional
|
16
|
-
from datetime import datetime, timedelta
|
17
|
-
|
18
|
-
from django.core.management.base import BaseCommand, CommandError
|
19
|
-
from django.db import transaction
|
20
|
-
from django.utils import timezone
|
21
|
-
from django.conf import settings
|
22
|
-
|
23
|
-
from django_cfg.modules.django_currency.database.database_loader import (
|
24
|
-
CurrencyDatabaseLoader,
|
25
|
-
DatabaseLoaderConfig,
|
26
|
-
create_database_loader
|
27
|
-
)
|
28
|
-
from django_cfg.apps.payments.models.currencies import Currency
|
29
|
-
|
30
|
-
logger = logging.getLogger(__name__)
|
31
|
-
|
32
|
-
|
33
|
-
class Command(BaseCommand):
|
34
|
-
"""
|
35
|
-
Management command to update currency data.
|
36
|
-
|
37
|
-
Features:
|
38
|
-
- Automatic detection of new currencies
|
39
|
-
- Rate updates for existing currencies
|
40
|
-
- Dry-run mode for testing
|
41
|
-
- Configurable limits for API calls
|
42
|
-
- Progress reporting
|
43
|
-
- Error handling and rollback
|
44
|
-
"""
|
45
|
-
|
46
|
-
help = 'Update currency data from external APIs (CoinGecko, YFinance)'
|
47
|
-
|
48
|
-
def add_arguments(self, parser):
|
49
|
-
"""Add command line arguments."""
|
50
|
-
parser.add_argument(
|
51
|
-
'--max-crypto',
|
52
|
-
type=int,
|
53
|
-
default=500,
|
54
|
-
help='Maximum number of cryptocurrencies to load (default: 500)'
|
55
|
-
)
|
56
|
-
|
57
|
-
parser.add_argument(
|
58
|
-
'--max-fiat',
|
59
|
-
type=int,
|
60
|
-
default=50,
|
61
|
-
help='Maximum number of fiat currencies to load (default: 50)'
|
62
|
-
)
|
63
|
-
|
64
|
-
parser.add_argument(
|
65
|
-
'--min-market-cap',
|
66
|
-
type=float,
|
67
|
-
default=1000000,
|
68
|
-
help='Minimum market cap in USD for cryptocurrencies (default: 1M)'
|
69
|
-
)
|
70
|
-
|
71
|
-
parser.add_argument(
|
72
|
-
'--dry-run',
|
73
|
-
action='store_true',
|
74
|
-
help='Show what would be updated without making changes'
|
75
|
-
)
|
76
|
-
|
77
|
-
parser.add_argument(
|
78
|
-
'--force-update',
|
79
|
-
action='store_true',
|
80
|
-
help='Force update all currencies even if recently updated'
|
81
|
-
)
|
82
|
-
|
83
|
-
parser.add_argument(
|
84
|
-
'--exclude-stablecoins',
|
85
|
-
action='store_true',
|
86
|
-
help='Exclude stablecoins from cryptocurrency updates'
|
87
|
-
)
|
88
|
-
|
89
|
-
parser.add_argument(
|
90
|
-
'--update-threshold-hours',
|
91
|
-
type=int,
|
92
|
-
default=6,
|
93
|
-
help='Only update currencies older than N hours (default: 6)'
|
94
|
-
)
|
95
|
-
|
96
|
-
parser.add_argument(
|
97
|
-
'--verbose',
|
98
|
-
action='store_true',
|
99
|
-
help='Verbose output with detailed progress'
|
100
|
-
)
|
101
|
-
|
102
|
-
def handle(self, *args, **options):
|
103
|
-
"""Main command handler."""
|
104
|
-
|
105
|
-
# Configure logging
|
106
|
-
log_level = logging.INFO if options['verbose'] else logging.WARNING
|
107
|
-
logging.getLogger('django_cfg.modules.django_currency').setLevel(log_level)
|
108
|
-
|
109
|
-
self.stdout.write(
|
110
|
-
self.style.SUCCESS('🏦 Starting currency database update...')
|
111
|
-
)
|
112
|
-
|
113
|
-
try:
|
114
|
-
# Create database loader with options
|
115
|
-
config = DatabaseLoaderConfig(
|
116
|
-
max_cryptocurrencies=options['max_crypto'],
|
117
|
-
max_fiat_currencies=options['max_fiat'],
|
118
|
-
min_market_cap_usd=options['min_market_cap'],
|
119
|
-
exclude_stablecoins=options['exclude_stablecoins'],
|
120
|
-
coingecko_delay=1.5, # Be respectful to APIs
|
121
|
-
yfinance_delay=0.5
|
122
|
-
)
|
123
|
-
|
124
|
-
loader = CurrencyDatabaseLoader(config)
|
125
|
-
|
126
|
-
# Get statistics
|
127
|
-
stats = loader.get_statistics()
|
128
|
-
self.stdout.write(f"📊 Loader config: {stats['total_currencies']} currencies available")
|
129
|
-
self.stdout.write(f" • {stats['total_fiat_currencies']} fiat currencies")
|
130
|
-
self.stdout.write(f" • {stats['total_cryptocurrencies']} cryptocurrencies")
|
131
|
-
|
132
|
-
# Check existing currencies
|
133
|
-
existing_count = Currency.objects.count()
|
134
|
-
self.stdout.write(f"📋 Current database: {existing_count} currencies")
|
135
|
-
|
136
|
-
# Determine update strategy
|
137
|
-
if options['force_update']:
|
138
|
-
currencies_to_update = Currency.objects.all()
|
139
|
-
self.stdout.write("🔄 Force update mode: updating all currencies")
|
140
|
-
else:
|
141
|
-
threshold = timezone.now() - timedelta(hours=options['update_threshold_hours'])
|
142
|
-
currencies_to_update = Currency.objects.filter(
|
143
|
-
rate_updated_at__lt=threshold
|
144
|
-
) | Currency.objects.filter(rate_updated_at__isnull=True)
|
145
|
-
|
146
|
-
self.stdout.write(
|
147
|
-
f"⏰ Updating currencies older than {options['update_threshold_hours']} hours: "
|
148
|
-
f"{currencies_to_update.count()} currencies"
|
149
|
-
)
|
150
|
-
|
151
|
-
# Load fresh currency data
|
152
|
-
self.stdout.write("🌐 Fetching fresh currency data from APIs...")
|
153
|
-
fresh_currencies = loader.build_currency_database_data()
|
154
|
-
|
155
|
-
if options['dry_run']:
|
156
|
-
self._handle_dry_run(fresh_currencies, currencies_to_update)
|
157
|
-
else:
|
158
|
-
self._handle_update(fresh_currencies, currencies_to_update, options)
|
159
|
-
|
160
|
-
except KeyboardInterrupt:
|
161
|
-
self.stdout.write(
|
162
|
-
self.style.WARNING('\n⚠️ Update interrupted by user')
|
163
|
-
)
|
164
|
-
raise CommandError("Update cancelled by user")
|
165
|
-
|
166
|
-
except Exception as e:
|
167
|
-
logger.exception("Currency update failed")
|
168
|
-
self.stdout.write(
|
169
|
-
self.style.ERROR(f'❌ Currency update failed: {str(e)}')
|
170
|
-
)
|
171
|
-
raise CommandError(f"Update failed: {str(e)}")
|
172
|
-
|
173
|
-
def _handle_dry_run(self, fresh_currencies: List, currencies_to_update):
|
174
|
-
"""Handle dry-run mode - show what would be updated."""
|
175
|
-
self.stdout.write(
|
176
|
-
self.style.WARNING('🧪 DRY RUN MODE - No changes will be made')
|
177
|
-
)
|
178
|
-
|
179
|
-
# Analyze changes
|
180
|
-
existing_codes = set(Currency.objects.values_list('code', flat=True))
|
181
|
-
fresh_codes = {curr.code for curr in fresh_currencies}
|
182
|
-
|
183
|
-
new_currencies = fresh_codes - existing_codes
|
184
|
-
existing_currencies = fresh_codes & existing_codes
|
185
|
-
|
186
|
-
self.stdout.write(f"\n📈 Analysis:")
|
187
|
-
self.stdout.write(f" • Would add {len(new_currencies)} new currencies")
|
188
|
-
self.stdout.write(f" • Would update {len(existing_currencies)} existing currencies")
|
189
|
-
|
190
|
-
if new_currencies:
|
191
|
-
self.stdout.write(f"\n➕ New currencies to add:")
|
192
|
-
for code in sorted(list(new_currencies)[:10]): # Show first 10
|
193
|
-
currency = next(c for c in fresh_currencies if c.code == code)
|
194
|
-
self.stdout.write(f" • {code}: {currency.name} ({currency.currency_type})")
|
195
|
-
if len(new_currencies) > 10:
|
196
|
-
self.stdout.write(f" ... and {len(new_currencies) - 10} more")
|
197
|
-
|
198
|
-
if existing_currencies:
|
199
|
-
self.stdout.write(f"\n🔄 Existing currencies to update:")
|
200
|
-
for code in sorted(list(existing_currencies)[:10]): # Show first 10
|
201
|
-
currency = next(c for c in fresh_currencies if c.code == code)
|
202
|
-
try:
|
203
|
-
existing = Currency.objects.get(code=code)
|
204
|
-
rate_diff = abs(existing.usd_rate - currency.usd_rate)
|
205
|
-
if rate_diff > 0.01: # Significant change
|
206
|
-
change_pct = ((currency.usd_rate - existing.usd_rate) / existing.usd_rate) * 100
|
207
|
-
self.stdout.write(
|
208
|
-
f" • {code}: ${existing.usd_rate:.6f} → ${currency.usd_rate:.6f} "
|
209
|
-
f"({change_pct:+.2f}%)"
|
210
|
-
)
|
211
|
-
except Currency.DoesNotExist:
|
212
|
-
pass
|
213
|
-
|
214
|
-
self.stdout.write(
|
215
|
-
self.style.SUCCESS('\n✅ Dry run completed - use --force-update to apply changes')
|
216
|
-
)
|
217
|
-
|
218
|
-
def _handle_update(self, fresh_currencies: List, currencies_to_update, options: Dict):
|
219
|
-
"""Handle actual database update."""
|
220
|
-
|
221
|
-
updated_count = 0
|
222
|
-
created_count = 0
|
223
|
-
errors = []
|
224
|
-
|
225
|
-
# Create lookup for fresh data
|
226
|
-
fresh_data_map = {curr.code: curr for curr in fresh_currencies}
|
227
|
-
|
228
|
-
try:
|
229
|
-
with transaction.atomic():
|
230
|
-
self.stdout.write("💾 Updating database...")
|
231
|
-
|
232
|
-
# Process currencies
|
233
|
-
for i, fresh_currency in enumerate(fresh_currencies):
|
234
|
-
try:
|
235
|
-
currency, created = Currency.objects.update_or_create(
|
236
|
-
code=fresh_currency.code,
|
237
|
-
defaults={
|
238
|
-
'name': fresh_currency.name,
|
239
|
-
'symbol': fresh_currency.symbol,
|
240
|
-
'currency_type': fresh_currency.currency_type,
|
241
|
-
'decimal_places': fresh_currency.decimal_places,
|
242
|
-
'usd_rate': fresh_currency.usd_rate,
|
243
|
-
'min_payment_amount': fresh_currency.min_payment_amount,
|
244
|
-
'is_active': fresh_currency.is_active,
|
245
|
-
'rate_updated_at': timezone.now()
|
246
|
-
}
|
247
|
-
)
|
248
|
-
|
249
|
-
if created:
|
250
|
-
created_count += 1
|
251
|
-
if options['verbose']:
|
252
|
-
self.stdout.write(f" ➕ Created {fresh_currency.code}")
|
253
|
-
else:
|
254
|
-
updated_count += 1
|
255
|
-
if options['verbose']:
|
256
|
-
self.stdout.write(f" 🔄 Updated {fresh_currency.code}")
|
257
|
-
|
258
|
-
# Progress indicator
|
259
|
-
if (i + 1) % 50 == 0:
|
260
|
-
self.stdout.write(f" Progress: {i + 1}/{len(fresh_currencies)} currencies processed")
|
261
|
-
|
262
|
-
except Exception as e:
|
263
|
-
error_msg = f"Failed to update {fresh_currency.code}: {str(e)}"
|
264
|
-
errors.append(error_msg)
|
265
|
-
logger.error(error_msg)
|
266
|
-
|
267
|
-
# Continue with other currencies unless it's a critical error
|
268
|
-
if len(errors) > 10: # Too many errors
|
269
|
-
raise CommandError(f"Too many errors ({len(errors)}), aborting")
|
270
|
-
|
271
|
-
# Summary
|
272
|
-
total_processed = created_count + updated_count
|
273
|
-
self.stdout.write(f"\n📊 Update Summary:")
|
274
|
-
self.stdout.write(f" ✅ Successfully processed: {total_processed} currencies")
|
275
|
-
self.stdout.write(f" ➕ Created new: {created_count}")
|
276
|
-
self.stdout.write(f" 🔄 Updated existing: {updated_count}")
|
277
|
-
|
278
|
-
if errors:
|
279
|
-
self.stdout.write(f" ⚠️ Errors: {len(errors)}")
|
280
|
-
for error in errors[:5]: # Show first 5 errors
|
281
|
-
self.stdout.write(f" • {error}")
|
282
|
-
if len(errors) > 5:
|
283
|
-
self.stdout.write(f" ... and {len(errors) - 5} more errors")
|
284
|
-
|
285
|
-
# Deactivate currencies not in fresh data (optional)
|
286
|
-
fresh_codes = {curr.code for curr in fresh_currencies}
|
287
|
-
stale_currencies = Currency.objects.filter(is_active=True).exclude(
|
288
|
-
code__in=fresh_codes
|
289
|
-
)
|
290
|
-
|
291
|
-
if stale_currencies.exists():
|
292
|
-
self.stdout.write(f" 📋 Found {stale_currencies.count()} currencies not in fresh data")
|
293
|
-
# Optionally deactivate them
|
294
|
-
# stale_currencies.update(is_active=False)
|
295
|
-
|
296
|
-
self.stdout.write(
|
297
|
-
self.style.SUCCESS('✅ Currency database update completed successfully!')
|
298
|
-
)
|
299
|
-
|
300
|
-
except Exception as e:
|
301
|
-
self.stdout.write(
|
302
|
-
self.style.ERROR(f'❌ Update failed and rolled back: {str(e)}')
|
303
|
-
)
|
304
|
-
raise
|
305
|
-
|
306
|
-
def _show_statistics(self):
|
307
|
-
"""Show current currency statistics."""
|
308
|
-
total = Currency.objects.count()
|
309
|
-
fiat_count = Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count()
|
310
|
-
crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
|
311
|
-
active_count = Currency.objects.filter(is_active=True).count()
|
312
|
-
|
313
|
-
# Recent updates
|
314
|
-
recent_threshold = timezone.now() - timedelta(hours=24)
|
315
|
-
recent_updates = Currency.objects.filter(rate_updated_at__gte=recent_threshold).count()
|
316
|
-
|
317
|
-
self.stdout.write(f"\n📊 Current Database Statistics:")
|
318
|
-
self.stdout.write(f" • Total currencies: {total}")
|
319
|
-
self.stdout.write(f" • Fiat currencies: {fiat_count}")
|
320
|
-
self.stdout.write(f" • Cryptocurrencies: {crypto_count}")
|
321
|
-
self.stdout.write(f" • Active currencies: {active_count}")
|
322
|
-
self.stdout.write(f" • Updated in last 24h: {recent_updates}")
|
323
|
-
|
324
|
-
# Show some examples
|
325
|
-
recent_currencies = Currency.objects.filter(
|
326
|
-
rate_updated_at__gte=recent_threshold
|
327
|
-
).order_by('-rate_updated_at')[:5]
|
328
|
-
|
329
|
-
if recent_currencies:
|
330
|
-
self.stdout.write(f"\n🕒 Recently updated currencies:")
|
331
|
-
for currency in recent_currencies:
|
332
|
-
age = timezone.now() - currency.rate_updated_at
|
333
|
-
hours_ago = int(age.total_seconds() / 3600)
|
334
|
-
self.stdout.write(
|
335
|
-
f" • {currency.code}: ${currency.usd_rate:.6f} ({hours_ago}h ago)"
|
336
|
-
)
|