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.
- 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/admin/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/config/__init__.py +65 -0
- django_cfg/apps/payments/config/module.py +70 -0
- django_cfg/apps/payments/config/providers.py +115 -0
- django_cfg/apps/payments/config/settings.py +96 -0
- django_cfg/apps/payments/config/utils.py +52 -0
- django_cfg/apps/payments/decorators.py +291 -0
- 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 +294 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +125 -11
- django_cfg/apps/payments/models/__init__.py +18 -0
- django_cfg/apps/payments/models/api_keys.py +2 -2
- django_cfg/apps/payments/models/balance.py +2 -2
- django_cfg/apps/payments/models/base.py +16 -0
- django_cfg/apps/payments/models/events.py +2 -2
- django_cfg/apps/payments/models/payments.py +112 -2
- django_cfg/apps/payments/models/subscriptions.py +2 -2
- django_cfg/apps/payments/services/__init__.py +64 -7
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +447 -0
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +576 -0
- django_cfg/apps/payments/services/core/subscription_service.py +614 -0
- django_cfg/apps/payments/services/internal_types.py +297 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -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 +22 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +103 -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/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +160 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/urls.py +5 -5
- django_cfg/apps/payments/utils/__init__.py +45 -0
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +245 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -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/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- 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/revolution.py +1 -1
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +12 -6
- 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/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -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.22.dist-info ā django_cfg-1.2.25.dist-info}/METADATA +11 -6
- {django_cfg-1.2.22.dist-info ā django_cfg-1.2.25.dist-info}/RECORD +113 -50
- 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/apps/payments/services/base.py +0 -68
- django_cfg/apps/payments/services/nowpayments.py +0 -78
- django_cfg/apps/payments/services/providers.py +0 -77
- django_cfg/apps/payments/services/redis_service.py +0 -215
- 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.22.dist-info ā django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.22.dist-info ā django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.22.dist-info ā django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,336 @@
|
|
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
|
+
)
|
@@ -1,32 +1,83 @@
|
|
1
1
|
"""
|
2
|
-
Currency
|
2
|
+
Manager for Currency model.
|
3
3
|
"""
|
4
4
|
|
5
5
|
from django.db import models
|
6
|
+
from django.utils import timezone
|
7
|
+
from datetime import timedelta
|
8
|
+
from typing import List, Optional
|
6
9
|
|
7
10
|
|
8
11
|
class CurrencyManager(models.Manager):
|
9
|
-
"""Manager for Currency model."""
|
12
|
+
"""Manager for Currency model with convenient query methods."""
|
10
13
|
|
11
|
-
def
|
12
|
-
"""Get active currencies."""
|
14
|
+
def active(self):
|
15
|
+
"""Get only active currencies."""
|
13
16
|
return self.filter(is_active=True)
|
14
17
|
|
15
|
-
def
|
16
|
-
"""Get fiat currencies."""
|
18
|
+
def fiat(self):
|
19
|
+
"""Get only fiat currencies."""
|
20
|
+
return self.filter(currency_type='fiat')
|
21
|
+
|
22
|
+
def crypto(self):
|
23
|
+
"""Get only cryptocurrencies."""
|
24
|
+
return self.filter(currency_type='crypto')
|
25
|
+
|
26
|
+
def active_fiat(self):
|
27
|
+
"""Get active fiat currencies."""
|
17
28
|
return self.filter(currency_type='fiat', is_active=True)
|
18
29
|
|
19
|
-
def
|
20
|
-
"""Get cryptocurrencies."""
|
30
|
+
def active_crypto(self):
|
31
|
+
"""Get active cryptocurrencies."""
|
21
32
|
return self.filter(currency_type='crypto', is_active=True)
|
33
|
+
|
34
|
+
def by_code(self, code: str):
|
35
|
+
"""Get currency by code (case insensitive)."""
|
36
|
+
return self.filter(code__iexact=code).first()
|
37
|
+
|
38
|
+
def supported_for_payments(self, min_amount: float = None):
|
39
|
+
"""Get currencies supported for payments."""
|
40
|
+
queryset = self.active()
|
41
|
+
if min_amount:
|
42
|
+
queryset = queryset.filter(min_payment_amount__lte=min_amount)
|
43
|
+
return queryset
|
44
|
+
|
45
|
+
def recently_updated(self, hours: int = 24):
|
46
|
+
"""Get currencies updated within the last N hours."""
|
47
|
+
threshold = timezone.now() - timedelta(hours=hours)
|
48
|
+
return self.filter(rate_updated_at__gte=threshold)
|
49
|
+
|
50
|
+
def outdated(self, days: int = 7):
|
51
|
+
"""Get currencies with outdated rates."""
|
52
|
+
threshold = timezone.now() - timedelta(days=days)
|
53
|
+
return self.filter(
|
54
|
+
models.Q(rate_updated_at__lt=threshold) |
|
55
|
+
models.Q(rate_updated_at__isnull=True)
|
56
|
+
)
|
57
|
+
|
58
|
+
def top_crypto_by_value(self, limit: int = 10):
|
59
|
+
"""Get top cryptocurrencies by USD value."""
|
60
|
+
return self.active_crypto().order_by('-usd_rate')[:limit]
|
61
|
+
|
62
|
+
def search(self, query: str):
|
63
|
+
"""Search currencies by code or name."""
|
64
|
+
return self.filter(
|
65
|
+
models.Q(code__icontains=query) |
|
66
|
+
models.Q(name__icontains=query)
|
67
|
+
)
|
22
68
|
|
23
69
|
|
24
70
|
class CurrencyNetworkManager(models.Manager):
|
25
71
|
"""Manager for CurrencyNetwork model."""
|
26
72
|
|
27
|
-
def
|
28
|
-
"""Get active networks."""
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
73
|
+
def active(self):
|
74
|
+
"""Get only active networks."""
|
75
|
+
return self.filter(is_active=True)
|
76
|
+
|
77
|
+
def for_currency(self, currency_code: str):
|
78
|
+
"""Get networks for a specific currency."""
|
79
|
+
return self.filter(currency__code__iexact=currency_code)
|
80
|
+
|
81
|
+
def active_for_currency(self, currency_code: str):
|
82
|
+
"""Get active networks for a specific currency."""
|
83
|
+
return self.active().filter(currency__code__iexact=currency_code)
|