django-cfg 1.3.7__py3-none-any.whl → 1.3.9__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/accounts/admin/__init__.py +24 -8
- django_cfg/apps/accounts/admin/activity_admin.py +146 -0
- django_cfg/apps/accounts/admin/filters.py +98 -22
- django_cfg/apps/accounts/admin/group_admin.py +86 -0
- django_cfg/apps/accounts/admin/inlines.py +42 -13
- django_cfg/apps/accounts/admin/otp_admin.py +115 -0
- django_cfg/apps/accounts/admin/registration_admin.py +173 -0
- django_cfg/apps/accounts/admin/resources.py +123 -19
- django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
- django_cfg/apps/accounts/admin/user_admin.py +362 -0
- django_cfg/apps/agents/admin/__init__.py +17 -4
- django_cfg/apps/agents/admin/execution_admin.py +204 -183
- django_cfg/apps/agents/admin/registry_admin.py +230 -255
- django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
- django_cfg/apps/agents/core/__init__.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +221 -0
- django_cfg/apps/agents/core/exceptions.py +14 -0
- django_cfg/apps/agents/core/orchestrator.py +18 -3
- django_cfg/apps/knowbase/admin/__init__.py +1 -1
- django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
- django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
- django_cfg/apps/knowbase/admin/document_admin.py +269 -262
- django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
- django_cfg/apps/knowbase/config/settings.py +21 -4
- django_cfg/apps/knowbase/views/chat_views.py +3 -0
- django_cfg/apps/leads/admin/__init__.py +3 -1
- django_cfg/apps/leads/admin/leads_admin.py +235 -35
- django_cfg/apps/maintenance/admin/__init__.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
- django_cfg/apps/maintenance/admin/log_admin.py +143 -61
- django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
- django_cfg/apps/maintenance/admin/site_admin.py +213 -352
- django_cfg/apps/newsletter/admin/__init__.py +29 -2
- django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
- django_cfg/apps/payments/admin/__init__.py +18 -27
- django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
- django_cfg/apps/payments/admin/balance_admin.py +166 -632
- django_cfg/apps/payments/admin/currencies_admin.py +235 -607
- django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
- django_cfg/apps/payments/admin/filters.py +83 -3
- django_cfg/apps/payments/admin/networks_admin.py +258 -0
- django_cfg/apps/payments/admin/payments_admin.py +171 -461
- django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
- django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
- django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
- django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
- django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
- django_cfg/apps/payments/middleware/api_access.py +32 -6
- django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
- django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
- django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
- django_cfg/apps/payments/models/balance.py +12 -0
- django_cfg/apps/payments/models/currencies.py +106 -32
- django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
- django_cfg/apps/payments/services/core/currency_service.py +35 -28
- django_cfg/apps/payments/services/core/payment_service.py +1 -1
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/base.py +95 -39
- django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
- django_cfg/apps/payments/services/providers/models/base.py +122 -0
- django_cfg/apps/payments/services/providers/models/providers.py +87 -0
- django_cfg/apps/payments/services/providers/models/universal.py +48 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
- django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
- django_cfg/apps/payments/services/providers/{nowpayments.py → nowpayments/provider.py} +240 -209
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
- django_cfg/apps/payments/services/providers/registry.py +4 -32
- django_cfg/apps/payments/services/providers/sync_service.py +277 -0
- django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
- django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
- django_cfg/apps/payments/tasks/__init__.py +39 -0
- django_cfg/apps/payments/tasks/types.py +73 -0
- django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
- django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
- django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
- django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
- django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
- django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
- django_cfg/apps/payments/urls_admin.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +5 -5
- django_cfg/apps/payments/views/overview/services.py +2 -2
- django_cfg/apps/payments/views/serializers/currencies.py +4 -3
- django_cfg/apps/support/admin/__init__.py +10 -1
- django_cfg/apps/support/admin/support_admin.py +338 -141
- django_cfg/apps/tasks/admin/__init__.py +11 -0
- django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +10 -5
- django_cfg/core/generation.py +1 -1
- django_cfg/management/commands/__init__.py +13 -1
- django_cfg/management/commands/app_agent_diagnose.py +470 -0
- django_cfg/management/commands/app_agent_generate.py +342 -0
- django_cfg/management/commands/app_agent_info.py +308 -0
- django_cfg/management/commands/migrate_all.py +9 -3
- django_cfg/management/commands/migrator.py +11 -6
- django_cfg/management/commands/rundramatiq.py +3 -2
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/models/api_keys.py +115 -0
- django_cfg/modules/django_admin/__init__.py +64 -0
- django_cfg/modules/django_admin/decorators/__init__.py +13 -0
- django_cfg/modules/django_admin/decorators/actions.py +106 -0
- django_cfg/modules/django_admin/decorators/display.py +106 -0
- django_cfg/modules/django_admin/mixins/__init__.py +14 -0
- django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
- django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
- django_cfg/modules/django_admin/models/__init__.py +20 -0
- django_cfg/modules/django_admin/models/action_models.py +33 -0
- django_cfg/modules/django_admin/models/badge_models.py +20 -0
- django_cfg/modules/django_admin/models/base.py +26 -0
- django_cfg/modules/django_admin/models/display_models.py +31 -0
- django_cfg/modules/django_admin/utils/badges.py +159 -0
- django_cfg/modules/django_admin/utils/displays.py +247 -0
- django_cfg/modules/django_app_agent/__init__.py +87 -0
- django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
- django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
- django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
- django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
- django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
- django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
- django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
- django_cfg/modules/django_app_agent/core/__init__.py +33 -0
- django_cfg/modules/django_app_agent/core/config.py +300 -0
- django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
- django_cfg/modules/django_app_agent/models/__init__.py +71 -0
- django_cfg/modules/django_app_agent/models/base.py +283 -0
- django_cfg/modules/django_app_agent/models/context.py +496 -0
- django_cfg/modules/django_app_agent/models/enums.py +481 -0
- django_cfg/modules/django_app_agent/models/requests.py +500 -0
- django_cfg/modules/django_app_agent/models/responses.py +585 -0
- django_cfg/modules/django_app_agent/pytest.ini +6 -0
- django_cfg/modules/django_app_agent/services/__init__.py +42 -0
- django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
- django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
- django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
- django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
- django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
- django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
- django_cfg/modules/django_app_agent/services/base.py +437 -0
- django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
- django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
- django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
- django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
- django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
- django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
- django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
- django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
- django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
- django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
- django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
- django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
- django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
- django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
- django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
- django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
- django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
- django_cfg/modules/django_app_agent/services/report_service.py +332 -0
- django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
- django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
- django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
- django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
- django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
- django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
- django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
- django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
- django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
- django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
- django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
- django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
- django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
- django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
- django_cfg/modules/django_app_agent/ui/cli.py +419 -0
- django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
- django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
- django_cfg/modules/django_app_agent/utils/logging.py +360 -0
- django_cfg/modules/django_app_agent/utils/validation.py +417 -0
- django_cfg/modules/django_currency/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
- django_cfg/modules/django_currency/core/converter.py +12 -12
- django_cfg/modules/django_currency/database/__init__.py +2 -2
- django_cfg/modules/django_currency/database/database_loader.py +93 -42
- django_cfg/modules/django_llm/llm/client.py +10 -2
- django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
- django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
- django_cfg/modules/django_unfold/dashboard.py +14 -13
- django_cfg/modules/django_unfold/models/config.py +1 -1
- django_cfg/registry/core.py +3 -0
- django_cfg/registry/third_party.py +2 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/RECORD +223 -117
- django_cfg/apps/accounts/admin/activity.py +0 -96
- django_cfg/apps/accounts/admin/group.py +0 -17
- django_cfg/apps/accounts/admin/otp.py +0 -59
- django_cfg/apps/accounts/admin/registration_source.py +0 -97
- django_cfg/apps/accounts/admin/twilio_response.py +0 -227
- django_cfg/apps/accounts/admin/user.py +0 -300
- django_cfg/apps/agents/core/agent.py +0 -281
- django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
- django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
- django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
- django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
- django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
- django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
- django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
- django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
- django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
- django_cfg/apps/tasks/admin.py +0 -320
- django_cfg/middleware/static_nocache.py +0 -55
- django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
- /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,370 +1,332 @@
|
|
1
1
|
"""
|
2
2
|
Currency management command for Universal Payment System v2.0.
|
3
3
|
|
4
|
-
|
4
|
+
Simple and reliable currency management using hybrid client.
|
5
5
|
"""
|
6
6
|
|
7
7
|
from django.core.management.base import BaseCommand, CommandError
|
8
|
-
from django.db import transaction
|
8
|
+
from django.db import transaction, models
|
9
9
|
from django.utils import timezone
|
10
|
-
from django.db.models import Q
|
11
10
|
from datetime import timedelta
|
12
|
-
from typing import List, Optional
|
13
11
|
import time
|
12
|
+
import concurrent.futures
|
13
|
+
from threading import Lock
|
14
14
|
|
15
15
|
from django_cfg.modules.django_logger import get_logger
|
16
|
-
from django_cfg.modules.django_currency import
|
17
|
-
|
18
|
-
CurrencyError, CurrencyNotFoundError
|
19
|
-
)
|
20
|
-
from django_cfg.apps.payments.models import Currency, Network, ProviderCurrency
|
16
|
+
from django_cfg.modules.django_currency import CurrencyConverter, CurrencyError
|
17
|
+
from django_cfg.apps.payments.models import Currency
|
21
18
|
|
22
19
|
logger = get_logger("manage_currencies")
|
23
20
|
|
24
21
|
|
25
22
|
class Command(BaseCommand):
|
26
|
-
"""
|
27
|
-
Universal currency management command using ready modules.
|
23
|
+
"""Simple currency management command using hybrid client."""
|
28
24
|
|
29
|
-
|
30
|
-
- Population of missing currencies
|
31
|
-
- USD rate updates using django_currency
|
32
|
-
- Provider currency synchronization
|
33
|
-
- Flexible filtering and options
|
34
|
-
"""
|
35
|
-
|
36
|
-
help = 'Manage currencies and exchange rates for the payment system'
|
25
|
+
help = 'Manage currencies and exchange rates using hybrid client'
|
37
26
|
|
38
27
|
def add_arguments(self, parser):
|
39
28
|
"""Add command arguments."""
|
40
|
-
|
41
|
-
# Main operation modes
|
42
29
|
parser.add_argument(
|
43
30
|
'--populate',
|
44
31
|
action='store_true',
|
45
|
-
help='Populate
|
32
|
+
help='Populate all supported currencies from hybrid client'
|
46
33
|
)
|
47
|
-
|
48
34
|
parser.add_argument(
|
49
35
|
'--rates-only',
|
50
36
|
action='store_true',
|
51
|
-
help='Update USD
|
37
|
+
help='Update USD rates only (no population)'
|
52
38
|
)
|
53
|
-
|
54
|
-
parser.add_argument(
|
55
|
-
'--sync-providers',
|
56
|
-
action='store_true',
|
57
|
-
help='Sync provider currencies after rate updates'
|
58
|
-
)
|
59
|
-
|
60
|
-
# Filtering options
|
61
39
|
parser.add_argument(
|
62
40
|
'--currency',
|
63
41
|
type=str,
|
64
|
-
help='Update specific currency
|
65
|
-
)
|
66
|
-
|
67
|
-
parser.add_argument(
|
68
|
-
'--currency-type',
|
69
|
-
choices=['fiat', 'crypto'],
|
70
|
-
help='Filter by currency type'
|
71
|
-
)
|
72
|
-
|
73
|
-
parser.add_argument(
|
74
|
-
'--provider',
|
75
|
-
type=str,
|
76
|
-
help='Filter by provider name'
|
42
|
+
help='Update specific currency only'
|
77
43
|
)
|
78
|
-
|
79
|
-
# Behavior options
|
80
44
|
parser.add_argument(
|
81
45
|
'--force',
|
82
46
|
action='store_true',
|
83
|
-
help='Force
|
47
|
+
help='Force update even if rates are fresh'
|
84
48
|
)
|
85
|
-
|
86
49
|
parser.add_argument(
|
87
|
-
'--
|
88
|
-
|
89
|
-
|
50
|
+
'--limit',
|
51
|
+
type=int,
|
52
|
+
default=500,
|
53
|
+
help='Limit number of currencies to process'
|
90
54
|
)
|
91
|
-
|
92
55
|
parser.add_argument(
|
93
|
-
'--
|
94
|
-
|
95
|
-
|
56
|
+
'--batch-size',
|
57
|
+
type=int,
|
58
|
+
default=20,
|
59
|
+
help='Number of currencies to process in parallel (default: 20)'
|
96
60
|
)
|
97
|
-
|
98
61
|
parser.add_argument(
|
99
|
-
'--
|
62
|
+
'--max-workers',
|
100
63
|
type=int,
|
101
|
-
default=
|
102
|
-
help='
|
64
|
+
default=10,
|
65
|
+
help='Maximum number of worker threads (default: 10)'
|
103
66
|
)
|
104
67
|
|
105
68
|
def handle(self, *args, **options):
|
106
69
|
"""Main command handler."""
|
107
|
-
|
108
70
|
start_time = time.time()
|
109
71
|
|
110
72
|
try:
|
111
|
-
self.stdout.write(
|
112
|
-
|
113
|
-
|
73
|
+
self.stdout.write(self.style.SUCCESS('🚀 Starting Universal Currency Management'))
|
74
|
+
|
75
|
+
# Initialize converter
|
76
|
+
self.converter = CurrencyConverter(cache_ttl=3600)
|
114
77
|
|
115
|
-
# Determine operation mode
|
116
78
|
if options['populate']:
|
117
|
-
|
79
|
+
self._populate_all_currencies(options)
|
118
80
|
elif options['rates_only']:
|
119
|
-
|
81
|
+
self._update_rates_only(options)
|
120
82
|
else:
|
121
83
|
# Default: populate + rates
|
122
|
-
self.
|
123
|
-
|
124
|
-
rates_result = self._update_rates_only(options)
|
125
|
-
result = populate_result + rates_result
|
126
|
-
|
127
|
-
# Optional provider sync
|
128
|
-
if options['sync_providers']:
|
129
|
-
self._sync_provider_currencies(options)
|
84
|
+
self._populate_all_currencies(options)
|
85
|
+
self._update_rates_only(options)
|
130
86
|
|
131
87
|
# Show summary
|
132
88
|
elapsed = time.time() - start_time
|
133
|
-
self.stdout.write(
|
134
|
-
|
135
|
-
f'✅ Currency management completed in {elapsed:.1f}s'
|
136
|
-
)
|
137
|
-
)
|
138
|
-
|
139
|
-
if not options['dry_run']:
|
140
|
-
self._show_final_stats()
|
89
|
+
self.stdout.write(self.style.SUCCESS(f'✅ Currency management completed in {elapsed:.1f}s'))
|
90
|
+
self._show_final_stats()
|
141
91
|
|
142
92
|
except Exception as e:
|
143
|
-
self.stdout.write(
|
144
|
-
self.style.ERROR(f'❌ Currency management failed: {e}')
|
145
|
-
)
|
93
|
+
self.stdout.write(self.style.ERROR(f'❌ Currency management failed: {e}'))
|
146
94
|
logger.error(f"Currency management command failed: {e}")
|
147
95
|
raise CommandError(f"Command failed: {e}")
|
148
96
|
|
149
|
-
def
|
150
|
-
"""Populate
|
151
|
-
|
97
|
+
def _populate_all_currencies(self, options):
|
98
|
+
"""Populate all supported currencies from hybrid client."""
|
152
99
|
self.stdout.write("📦 Populating base currencies...")
|
153
100
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
(
|
158
|
-
('EUR', 'Euro', Currency.CurrencyType.FIAT, '€', 2),
|
159
|
-
('GBP', 'British Pound', Currency.CurrencyType.FIAT, '£', 2),
|
160
|
-
('JPY', 'Japanese Yen', Currency.CurrencyType.FIAT, '¥', 0),
|
161
|
-
('CNY', 'Chinese Yuan', Currency.CurrencyType.FIAT, '¥', 2),
|
162
|
-
('RUB', 'Russian Ruble', Currency.CurrencyType.FIAT, '₽', 2),
|
101
|
+
try:
|
102
|
+
# Get all supported currencies from hybrid client
|
103
|
+
supported_currencies = self.converter.hybrid.get_all_supported_currencies()
|
104
|
+
self.stdout.write(f"Found {len(supported_currencies)} supported currencies")
|
163
105
|
|
164
|
-
#
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
('ADA', 'Cardano', Currency.CurrencyType.CRYPTO, '', 6),
|
171
|
-
('SOL', 'Solana', Currency.CurrencyType.CRYPTO, '', 8),
|
172
|
-
('DOT', 'Polkadot', Currency.CurrencyType.CRYPTO, '', 8),
|
173
|
-
('MATIC', 'Polygon', Currency.CurrencyType.CRYPTO, '', 8),
|
174
|
-
('LTC', 'Litecoin', Currency.CurrencyType.CRYPTO, 'Ł', 8),
|
175
|
-
('TRX', 'TRON', Currency.CurrencyType.CRYPTO, '', 6),
|
176
|
-
('XRP', 'Ripple', Currency.CurrencyType.CRYPTO, '', 6),
|
177
|
-
]
|
178
|
-
|
179
|
-
# Apply currency type filter
|
180
|
-
if options['currency_type']:
|
181
|
-
currency_type_filter = Currency.CurrencyType.FIAT if options['currency_type'] == 'fiat' else Currency.CurrencyType.CRYPTO
|
182
|
-
standard_currencies = [
|
183
|
-
c for c in standard_currencies
|
184
|
-
if c[2] == currency_type_filter
|
185
|
-
]
|
186
|
-
|
187
|
-
# Apply specific currency filter
|
188
|
-
if options['currency']:
|
189
|
-
currency_code = options['currency'].upper()
|
190
|
-
standard_currencies = [
|
191
|
-
c for c in standard_currencies
|
192
|
-
if c[0] == currency_code
|
193
|
-
]
|
106
|
+
# Apply limit
|
107
|
+
if options['limit'] and len(supported_currencies) > options['limit']:
|
108
|
+
# Take first N currencies (sorted alphabetically)
|
109
|
+
limited_currencies = dict(list(supported_currencies.items())[:options['limit']])
|
110
|
+
supported_currencies = limited_currencies
|
111
|
+
self.stdout.write(f"Limited to {len(supported_currencies)} currencies")
|
194
112
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
for code, name, currency_type, symbol, decimal_places in standard_currencies:
|
202
|
-
|
203
|
-
if options['dry_run']:
|
204
|
-
exists = Currency.objects.filter(code=code).exists()
|
205
|
-
if exists and options['skip_existing']:
|
206
|
-
self.stdout.write(f" [DRY RUN] Would skip existing {code}")
|
207
|
-
skipped_count += 1
|
113
|
+
# Apply specific currency filter
|
114
|
+
if options['currency']:
|
115
|
+
currency_code = options['currency'].upper()
|
116
|
+
if currency_code in supported_currencies:
|
117
|
+
supported_currencies = {currency_code: supported_currencies[currency_code]}
|
208
118
|
else:
|
209
|
-
|
210
|
-
continue
|
119
|
+
raise CommandError(f"Currency '{currency_code}' not supported by hybrid client")
|
211
120
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
)
|
223
|
-
|
224
|
-
if created:
|
225
|
-
self.stdout.write(f" ✅ Created {code} - {name}")
|
226
|
-
created_count += 1
|
227
|
-
logger.info(f"Created currency: {code}")
|
228
|
-
elif not options['skip_existing']:
|
229
|
-
# Update existing currency if not skipping
|
230
|
-
currency.name = name
|
231
|
-
currency.symbol = symbol
|
232
|
-
currency.decimal_places = decimal_places
|
233
|
-
currency.save()
|
234
|
-
self.stdout.write(f" 🔄 Updated {code} - {name}")
|
235
|
-
else:
|
236
|
-
self.stdout.write(f" ⏭️ Skipped existing {code}")
|
237
|
-
skipped_count += 1
|
121
|
+
created_count = 0
|
122
|
+
updated_count = 0
|
123
|
+
skipped_count = 0
|
124
|
+
|
125
|
+
for code, name in supported_currencies.items():
|
126
|
+
try:
|
127
|
+
# Determine currency type based on code
|
128
|
+
currency_type = self._determine_currency_type(code)
|
129
|
+
decimal_places = self._get_decimal_places(code, currency_type)
|
130
|
+
symbol = self._get_currency_symbol(code)
|
238
131
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
132
|
+
currency, created = Currency.objects.get_or_create(
|
133
|
+
code=code,
|
134
|
+
defaults={
|
135
|
+
'name': name,
|
136
|
+
'currency_type': currency_type,
|
137
|
+
'symbol': symbol,
|
138
|
+
'decimal_places': decimal_places,
|
139
|
+
'is_active': True
|
140
|
+
}
|
141
|
+
)
|
142
|
+
|
143
|
+
if created:
|
144
|
+
self.stdout.write(f" ✅ Created {code} - {name}")
|
145
|
+
created_count += 1
|
146
|
+
logger.info(f"Created currency: {code}")
|
147
|
+
else:
|
148
|
+
# Update name if it's different
|
149
|
+
if currency.name != name:
|
150
|
+
currency.name = name
|
151
|
+
currency.save()
|
152
|
+
self.stdout.write(f" 🔄 Updated {code} - {name}")
|
153
|
+
updated_count += 1
|
154
|
+
else:
|
155
|
+
self.stdout.write(f" ⏭️ Skipped existing {code}")
|
156
|
+
skipped_count += 1
|
157
|
+
|
158
|
+
except Exception as e:
|
159
|
+
self.stdout.write(f" ❌ Failed to create {code}: {e}")
|
160
|
+
logger.error(f"Failed to create currency {code}: {e}")
|
161
|
+
|
162
|
+
self.stdout.write(f"📦 Population complete: {created_count} created, {updated_count} updated, {skipped_count} skipped")
|
163
|
+
|
164
|
+
except Exception as e:
|
165
|
+
self.stdout.write(self.style.ERROR(f"Failed to populate currencies: {e}"))
|
166
|
+
logger.error(f"Currency population failed: {e}")
|
167
|
+
raise
|
245
168
|
|
246
|
-
def _update_rates_only(self, options)
|
247
|
-
"""Update USD exchange rates using
|
248
|
-
|
169
|
+
def _update_rates_only(self, options):
|
170
|
+
"""Update USD exchange rates for existing currencies using batch processing."""
|
249
171
|
self.stdout.write("💱 Updating USD exchange rates...")
|
250
172
|
|
251
|
-
#
|
252
|
-
queryset = Currency.objects.
|
173
|
+
# Get currencies to update
|
174
|
+
queryset = Currency.objects.filter(is_active=True)
|
253
175
|
|
254
176
|
if options['currency']:
|
255
|
-
|
177
|
+
currency_code = options['currency'].upper()
|
178
|
+
queryset = queryset.filter(code=currency_code)
|
256
179
|
if not queryset.exists():
|
257
|
-
raise CommandError(f"Currency '{
|
180
|
+
raise CommandError(f"Currency '{currency_code}' not found in database")
|
258
181
|
|
259
|
-
|
260
|
-
currency_type = Currency.CurrencyType.FIAT if options['currency_type'] == 'fiat' else Currency.CurrencyType.CRYPTO
|
261
|
-
queryset = queryset.filter(currency_type=currency_type)
|
262
|
-
|
263
|
-
# Filter by staleness unless forced
|
182
|
+
# Apply freshness filter unless forced
|
264
183
|
if not options['force']:
|
265
|
-
#
|
266
|
-
|
267
|
-
|
184
|
+
# Only update rates older than 1 hour
|
185
|
+
stale_threshold = timezone.now() - timedelta(hours=1)
|
186
|
+
queryset = queryset.filter(
|
187
|
+
models.Q(usd_rate_updated_at__isnull=True) |
|
188
|
+
models.Q(usd_rate_updated_at__lt=stale_threshold)
|
189
|
+
)
|
190
|
+
|
191
|
+
currencies = list(queryset[:options['limit']])
|
192
|
+
self.stdout.write(f"📊 Processing {len(currencies)} currencies...")
|
268
193
|
|
269
|
-
#
|
270
|
-
|
194
|
+
# Handle USD separately (always 1.0)
|
195
|
+
usd_currencies = [c for c in currencies if c.code == 'USD']
|
196
|
+
other_currencies = [c for c in currencies if c.code != 'USD']
|
271
197
|
|
272
198
|
updated_count = 0
|
273
199
|
error_count = 0
|
274
200
|
|
275
|
-
|
276
|
-
|
277
|
-
|
201
|
+
# Update USD currencies first (instant)
|
202
|
+
for currency in usd_currencies:
|
203
|
+
currency.usd_rate = 1.0
|
204
|
+
currency.usd_rate_updated_at = timezone.now()
|
205
|
+
currency.save()
|
206
|
+
self.stdout.write(f" ✅ USD: $1.00000000")
|
207
|
+
updated_count += 1
|
208
|
+
|
209
|
+
# Process other currencies in batches with threading
|
210
|
+
if other_currencies:
|
211
|
+
batch_size = options['batch_size']
|
212
|
+
max_workers = min(options['max_workers'], len(other_currencies))
|
278
213
|
|
279
|
-
|
280
|
-
self.stdout.write(f" [DRY RUN] Would update {currency.code}")
|
281
|
-
continue
|
214
|
+
self.stdout.write(f"🚀 Using {max_workers} workers, batch size: {batch_size}")
|
282
215
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
216
|
+
# Thread-safe counters
|
217
|
+
self._lock = Lock()
|
218
|
+
self._updated_count = 0
|
219
|
+
self._error_count = 0
|
220
|
+
|
221
|
+
# Process in batches
|
222
|
+
for i in range(0, len(other_currencies), batch_size):
|
223
|
+
batch = other_currencies[i:i + batch_size]
|
224
|
+
self.stdout.write(f"📦 Processing batch {i//batch_size + 1}/{(len(other_currencies) + batch_size - 1)//batch_size} ({len(batch)} currencies)")
|
291
225
|
|
292
|
-
|
293
|
-
#
|
294
|
-
|
295
|
-
currency
|
296
|
-
|
297
|
-
|
298
|
-
defaults={
|
299
|
-
'is_enabled': True
|
300
|
-
}
|
301
|
-
)
|
302
|
-
|
303
|
-
if not created:
|
304
|
-
# TODO: Add rate tracking fields to ProviderCurrency model
|
305
|
-
provider_currency.save() # Touch the record to update timestamp
|
306
|
-
|
307
|
-
# Update currency's exchange rate source
|
308
|
-
currency.exchange_rate_source = 'django_currency'
|
309
|
-
currency.save(update_fields=['exchange_rate_source'])
|
310
|
-
|
311
|
-
self.stdout.write(f" ✅ {currency.code}: ${usd_rate:.8f}")
|
312
|
-
updated_count += 1
|
313
|
-
|
314
|
-
else:
|
315
|
-
self.stdout.write(f" ⚠️ {currency.code}: No rate available")
|
226
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
227
|
+
# Submit all tasks in current batch
|
228
|
+
future_to_currency = {
|
229
|
+
executor.submit(self._update_single_rate, currency): currency
|
230
|
+
for currency in batch
|
231
|
+
}
|
316
232
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
233
|
+
# Process completed tasks
|
234
|
+
for future in concurrent.futures.as_completed(future_to_currency):
|
235
|
+
currency = future_to_currency[future]
|
236
|
+
try:
|
237
|
+
success = future.result()
|
238
|
+
if success:
|
239
|
+
with self._lock:
|
240
|
+
self._updated_count += 1
|
241
|
+
else:
|
242
|
+
with self._lock:
|
243
|
+
self._error_count += 1
|
244
|
+
except Exception as e:
|
245
|
+
self.stdout.write(f" ❌ {currency.code}: Thread error: {e}")
|
246
|
+
with self._lock:
|
247
|
+
self._error_count += 1
|
248
|
+
|
249
|
+
# Show batch progress
|
250
|
+
with self._lock:
|
251
|
+
self.stdout.write(f" 📊 Batch complete: {self._updated_count} updated, {self._error_count} errors so far")
|
252
|
+
|
253
|
+
updated_count += self._updated_count
|
254
|
+
error_count += self._error_count
|
324
255
|
|
325
256
|
self.stdout.write(f"💱 Rate update complete: {updated_count} updated, {error_count} errors")
|
326
|
-
return updated_count
|
327
257
|
|
328
|
-
def
|
329
|
-
"""
|
330
|
-
|
331
|
-
self.stdout.write("🔄 Syncing provider currencies...")
|
332
|
-
|
258
|
+
def _update_single_rate(self, currency):
|
259
|
+
"""Update rate for a single currency (thread-safe)."""
|
333
260
|
try:
|
334
|
-
from
|
261
|
+
# Get rate from hybrid client
|
262
|
+
rate_obj = self.converter.hybrid.fetch_rate(currency.code, 'USD')
|
335
263
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
self.stdout.write("
|
264
|
+
# Update database (Django ORM is thread-safe for individual operations)
|
265
|
+
currency.usd_rate = rate_obj.rate
|
266
|
+
currency.usd_rate_updated_at = timezone.now()
|
267
|
+
currency.save()
|
268
|
+
|
269
|
+
self.stdout.write(f" ✅ {currency.code}: ${rate_obj.rate:.8f}")
|
270
|
+
return True
|
342
271
|
|
343
272
|
except Exception as e:
|
344
|
-
self.stdout.write(f"⚠️
|
345
|
-
logger.warning(f"
|
273
|
+
self.stdout.write(f" ⚠️ {currency.code}: Failed to convert 1.0 {currency.code} to USD: {e}")
|
274
|
+
logger.warning(f"Rate update failed for {currency.code}: {e}")
|
275
|
+
return False
|
276
|
+
|
277
|
+
def _determine_currency_type(self, code: str) -> str:
|
278
|
+
"""Determine currency type based on code."""
|
279
|
+
crypto_currencies = {
|
280
|
+
'BTC', 'ETH', 'BNB', 'XRP', 'ADA', 'SOL', 'DOT', 'MATIC', 'LTC', 'BCH',
|
281
|
+
'LINK', 'UNI', 'ATOM', 'XLM', 'VET', 'FIL', 'TRX', 'ETC', 'THETA',
|
282
|
+
'AAVE', 'MKR', 'COMP', 'SUSHI', 'USDT', 'USDC', 'BUSD', 'DAI', 'TUSD', 'USDP'
|
283
|
+
}
|
284
|
+
|
285
|
+
metal_currencies = {'XAU', 'XAG', 'XPT', 'XPD'}
|
286
|
+
|
287
|
+
if code in crypto_currencies:
|
288
|
+
return Currency.CurrencyType.CRYPTO
|
289
|
+
elif code in metal_currencies:
|
290
|
+
return Currency.CurrencyType.FIAT # Metals are treated as fiat for now
|
291
|
+
else:
|
292
|
+
return Currency.CurrencyType.FIAT
|
293
|
+
|
294
|
+
def _get_decimal_places(self, code: str, currency_type: str) -> int:
|
295
|
+
"""Get appropriate decimal places for currency."""
|
296
|
+
if currency_type == Currency.CurrencyType.CRYPTO:
|
297
|
+
# Most cryptos use 8 decimal places, stablecoins use 6
|
298
|
+
stablecoins = {'USDT', 'USDC', 'BUSD', 'DAI', 'TUSD', 'USDP'}
|
299
|
+
return 6 if code in stablecoins else 8
|
300
|
+
elif code == 'JPY':
|
301
|
+
# Japanese Yen has no decimal places
|
302
|
+
return 0
|
303
|
+
else:
|
304
|
+
# Most fiat currencies use 2 decimal places
|
305
|
+
return 2
|
306
|
+
|
307
|
+
def _get_currency_symbol(self, code: str) -> str:
|
308
|
+
"""Get currency symbol if known."""
|
309
|
+
symbols = {
|
310
|
+
'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥', 'RUB': '₽',
|
311
|
+
'BTC': '₿', 'ETH': 'Ξ', 'LTC': 'Ł', 'USDT': '₮'
|
312
|
+
}
|
313
|
+
return symbols.get(code, '')
|
346
314
|
|
347
315
|
def _show_final_stats(self):
|
348
316
|
"""Show final statistics."""
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
self.stdout.write(f" Fiat: {fiat_count}, Crypto: {crypto_count}")
|
366
|
-
self.stdout.write(f" Active: {active_count}")
|
367
|
-
self.stdout.write(f" With USD rates: {currencies_with_rates} ({rate_coverage:.1f}%)")
|
368
|
-
|
369
|
-
except Exception as e:
|
370
|
-
logger.warning(f"Failed to show final stats: {e}")
|
317
|
+
total_currencies = Currency.objects.count()
|
318
|
+
active_currencies = Currency.objects.filter(is_active=True).count()
|
319
|
+
|
320
|
+
# Count by type
|
321
|
+
fiat_count = Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count()
|
322
|
+
crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
|
323
|
+
|
324
|
+
# Count with USD rates
|
325
|
+
with_rates = Currency.objects.filter(usd_rate__isnull=False, usd_rate__gt=0).count()
|
326
|
+
rate_percentage = (with_rates / total_currencies * 100) if total_currencies > 0 else 0
|
327
|
+
|
328
|
+
self.stdout.write("\n📊 Final Statistics:")
|
329
|
+
self.stdout.write(f" Total currencies: {total_currencies}")
|
330
|
+
self.stdout.write(f" Fiat: {fiat_count}, Crypto: {crypto_count}")
|
331
|
+
self.stdout.write(f" Active: {active_currencies}")
|
332
|
+
self.stdout.write(f" With USD rates: {with_rates} ({rate_percentage:.1f}%)")
|
@@ -13,7 +13,10 @@ import time
|
|
13
13
|
|
14
14
|
from django_cfg.modules.django_logger import get_logger
|
15
15
|
from django_cfg.apps.payments.models import Currency, Network, ProviderCurrency
|
16
|
-
from django_cfg.apps.payments.services.providers import
|
16
|
+
from django_cfg.apps.payments.services.providers import (
|
17
|
+
get_provider_registry,
|
18
|
+
get_provider_sync_service
|
19
|
+
)
|
17
20
|
|
18
21
|
logger = get_logger("manage_providers")
|
19
22
|
|