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.
Files changed (251) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +24 -8
  3. django_cfg/apps/accounts/admin/activity_admin.py +146 -0
  4. django_cfg/apps/accounts/admin/filters.py +98 -22
  5. django_cfg/apps/accounts/admin/group_admin.py +86 -0
  6. django_cfg/apps/accounts/admin/inlines.py +42 -13
  7. django_cfg/apps/accounts/admin/otp_admin.py +115 -0
  8. django_cfg/apps/accounts/admin/registration_admin.py +173 -0
  9. django_cfg/apps/accounts/admin/resources.py +123 -19
  10. django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
  11. django_cfg/apps/accounts/admin/user_admin.py +362 -0
  12. django_cfg/apps/agents/admin/__init__.py +17 -4
  13. django_cfg/apps/agents/admin/execution_admin.py +204 -183
  14. django_cfg/apps/agents/admin/registry_admin.py +230 -255
  15. django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
  16. django_cfg/apps/agents/core/__init__.py +1 -1
  17. django_cfg/apps/agents/core/django_agent.py +221 -0
  18. django_cfg/apps/agents/core/exceptions.py +14 -0
  19. django_cfg/apps/agents/core/orchestrator.py +18 -3
  20. django_cfg/apps/knowbase/admin/__init__.py +1 -1
  21. django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
  22. django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
  23. django_cfg/apps/knowbase/admin/document_admin.py +269 -262
  24. django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
  25. django_cfg/apps/knowbase/config/settings.py +21 -4
  26. django_cfg/apps/knowbase/views/chat_views.py +3 -0
  27. django_cfg/apps/leads/admin/__init__.py +3 -1
  28. django_cfg/apps/leads/admin/leads_admin.py +235 -35
  29. django_cfg/apps/maintenance/admin/__init__.py +2 -2
  30. django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
  31. django_cfg/apps/maintenance/admin/log_admin.py +143 -61
  32. django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
  33. django_cfg/apps/maintenance/admin/site_admin.py +213 -352
  34. django_cfg/apps/newsletter/admin/__init__.py +29 -2
  35. django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
  36. django_cfg/apps/payments/admin/__init__.py +18 -27
  37. django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
  38. django_cfg/apps/payments/admin/balance_admin.py +166 -632
  39. django_cfg/apps/payments/admin/currencies_admin.py +235 -607
  40. django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
  41. django_cfg/apps/payments/admin/filters.py +83 -3
  42. django_cfg/apps/payments/admin/networks_admin.py +258 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +171 -461
  44. django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
  45. django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
  46. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
  48. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  49. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
  50. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  51. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  52. django_cfg/apps/payments/middleware/api_access.py +32 -6
  53. django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
  54. django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
  55. django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
  56. django_cfg/apps/payments/models/balance.py +12 -0
  57. django_cfg/apps/payments/models/currencies.py +106 -32
  58. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  59. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  60. django_cfg/apps/payments/services/core/payment_service.py +1 -1
  61. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  62. django_cfg/apps/payments/services/providers/base.py +95 -39
  63. django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
  64. django_cfg/apps/payments/services/providers/models/base.py +122 -0
  65. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  66. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  67. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  68. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  69. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  70. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  71. django_cfg/apps/payments/services/providers/{nowpayments.py → nowpayments/provider.py} +240 -209
  72. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  73. django_cfg/apps/payments/services/providers/registry.py +4 -32
  74. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  75. django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
  76. django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
  77. django_cfg/apps/payments/tasks/__init__.py +39 -0
  78. django_cfg/apps/payments/tasks/types.py +73 -0
  79. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  80. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  81. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  82. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  83. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  84. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  85. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  86. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  87. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  88. django_cfg/apps/payments/urls_admin.py +1 -1
  89. django_cfg/apps/payments/views/api/currencies.py +5 -5
  90. django_cfg/apps/payments/views/overview/services.py +2 -2
  91. django_cfg/apps/payments/views/serializers/currencies.py +4 -3
  92. django_cfg/apps/support/admin/__init__.py +10 -1
  93. django_cfg/apps/support/admin/support_admin.py +338 -141
  94. django_cfg/apps/tasks/admin/__init__.py +11 -0
  95. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  96. django_cfg/config.py +1 -1
  97. django_cfg/core/config.py +10 -5
  98. django_cfg/core/generation.py +1 -1
  99. django_cfg/management/commands/__init__.py +13 -1
  100. django_cfg/management/commands/app_agent_diagnose.py +470 -0
  101. django_cfg/management/commands/app_agent_generate.py +342 -0
  102. django_cfg/management/commands/app_agent_info.py +308 -0
  103. django_cfg/management/commands/migrate_all.py +9 -3
  104. django_cfg/management/commands/migrator.py +11 -6
  105. django_cfg/management/commands/rundramatiq.py +3 -2
  106. django_cfg/middleware/__init__.py +0 -2
  107. django_cfg/models/api_keys.py +115 -0
  108. django_cfg/modules/django_admin/__init__.py +64 -0
  109. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  110. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  111. django_cfg/modules/django_admin/decorators/display.py +106 -0
  112. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  113. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  114. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  115. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  116. django_cfg/modules/django_admin/models/__init__.py +20 -0
  117. django_cfg/modules/django_admin/models/action_models.py +33 -0
  118. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  119. django_cfg/modules/django_admin/models/base.py +26 -0
  120. django_cfg/modules/django_admin/models/display_models.py +31 -0
  121. django_cfg/modules/django_admin/utils/badges.py +159 -0
  122. django_cfg/modules/django_admin/utils/displays.py +247 -0
  123. django_cfg/modules/django_app_agent/__init__.py +87 -0
  124. django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
  125. django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
  126. django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
  127. django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
  128. django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
  129. django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
  130. django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
  131. django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
  132. django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
  133. django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
  134. django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
  135. django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
  136. django_cfg/modules/django_app_agent/core/__init__.py +33 -0
  137. django_cfg/modules/django_app_agent/core/config.py +300 -0
  138. django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
  139. django_cfg/modules/django_app_agent/models/__init__.py +71 -0
  140. django_cfg/modules/django_app_agent/models/base.py +283 -0
  141. django_cfg/modules/django_app_agent/models/context.py +496 -0
  142. django_cfg/modules/django_app_agent/models/enums.py +481 -0
  143. django_cfg/modules/django_app_agent/models/requests.py +500 -0
  144. django_cfg/modules/django_app_agent/models/responses.py +585 -0
  145. django_cfg/modules/django_app_agent/pytest.ini +6 -0
  146. django_cfg/modules/django_app_agent/services/__init__.py +42 -0
  147. django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
  148. django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
  149. django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
  150. django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
  151. django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
  152. django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
  153. django_cfg/modules/django_app_agent/services/base.py +437 -0
  154. django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
  155. django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
  156. django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
  157. django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
  158. django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
  159. django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
  160. django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
  161. django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
  162. django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
  163. django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
  164. django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
  165. django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
  166. django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
  167. django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
  168. django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
  169. django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
  170. django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
  171. django_cfg/modules/django_app_agent/services/report_service.py +332 -0
  172. django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
  173. django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
  174. django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
  175. django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
  176. django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
  177. django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
  178. django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
  179. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
  180. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
  181. django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
  182. django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
  183. django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
  184. django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
  185. django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
  186. django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
  187. django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
  188. django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
  189. django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
  190. django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
  191. django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
  192. django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
  193. django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
  194. django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
  195. django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
  196. django_cfg/modules/django_app_agent/ui/cli.py +419 -0
  197. django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
  198. django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
  199. django_cfg/modules/django_app_agent/utils/logging.py +360 -0
  200. django_cfg/modules/django_app_agent/utils/validation.py +417 -0
  201. django_cfg/modules/django_currency/__init__.py +2 -2
  202. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  203. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  204. django_cfg/modules/django_currency/core/converter.py +12 -12
  205. django_cfg/modules/django_currency/database/__init__.py +2 -2
  206. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  207. django_cfg/modules/django_llm/llm/client.py +10 -2
  208. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  209. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  210. django_cfg/modules/django_unfold/dashboard.py +14 -13
  211. django_cfg/modules/django_unfold/models/config.py +1 -1
  212. django_cfg/registry/core.py +3 -0
  213. django_cfg/registry/third_party.py +2 -2
  214. django_cfg/template_archive/django_sample.zip +0 -0
  215. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
  216. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/RECORD +223 -117
  217. django_cfg/apps/accounts/admin/activity.py +0 -96
  218. django_cfg/apps/accounts/admin/group.py +0 -17
  219. django_cfg/apps/accounts/admin/otp.py +0 -59
  220. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  221. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  222. django_cfg/apps/accounts/admin/user.py +0 -300
  223. django_cfg/apps/agents/core/agent.py +0 -281
  224. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  225. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  226. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  227. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  228. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  229. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  230. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  231. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  232. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  233. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  234. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  235. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  236. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  237. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  238. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  239. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  240. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  241. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  242. django_cfg/apps/tasks/admin.py +0 -320
  243. django_cfg/middleware/static_nocache.py +0 -55
  244. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  245. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  246. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  247. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  248. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  249. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
  250. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
  251. {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
- Integrates with django_currency module for automatic rate updates and population.
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
- CurrencyConverter, convert_currency, get_exchange_rate,
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
- Features:
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 missing base currencies'
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 exchange rates only (no population)'
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 code (e.g., BTC, ETH)'
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 refresh rates even if recently updated'
47
+ help='Force update even if rates are fresh'
84
48
  )
85
-
86
49
  parser.add_argument(
87
- '--skip-existing',
88
- action='store_true',
89
- help='Skip currencies that already exist during population'
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
- '--dry-run',
94
- action='store_true',
95
- help='Show what would be done without making changes'
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
- '--limit',
62
+ '--max-workers',
100
63
  type=int,
101
- default=100,
102
- help='Limit number of currencies to process (default: 100)'
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
- self.style.SUCCESS('🚀 Starting Universal Currency Management')
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
- result = self._populate_currencies(options)
79
+ self._populate_all_currencies(options)
118
80
  elif options['rates_only']:
119
- result = self._update_rates_only(options)
81
+ self._update_rates_only(options)
120
82
  else:
121
83
  # Default: populate + rates
122
- self.stdout.write("📋 Running full currency management (populate + rates)")
123
- populate_result = self._populate_currencies(options)
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
- self.style.SUCCESS(
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 _populate_currencies(self, options) -> int:
150
- """Populate missing base currencies."""
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
- # Define standard currencies to populate
155
- standard_currencies = [
156
- # Major fiat currencies
157
- ('USD', 'US Dollar', Currency.CurrencyType.FIAT, '$', 2),
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
- # Major cryptocurrencies
165
- ('BTC', 'Bitcoin', Currency.CurrencyType.CRYPTO, '', 8),
166
- ('ETH', 'Ethereum', Currency.CurrencyType.CRYPTO, 'Ξ', 8),
167
- ('USDT', 'Tether USD', Currency.CurrencyType.CRYPTO, '', 6),
168
- ('USDC', 'USD Coin', Currency.CurrencyType.CRYPTO, '', 6),
169
- ('BNB', 'Binance Coin', Currency.CurrencyType.CRYPTO, '', 8),
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
- if not standard_currencies:
196
- raise CommandError(f"Currency '{currency_code}' not in standard list")
197
-
198
- created_count = 0
199
- skipped_count = 0
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
- self.stdout.write(f" [DRY RUN] Would create/update {code}")
210
- continue
119
+ raise CommandError(f"Currency '{currency_code}' not supported by hybrid client")
211
120
 
212
- try:
213
- currency, created = Currency.objects.get_or_create(
214
- code=code,
215
- defaults={
216
- 'name': name,
217
- 'currency_type': currency_type,
218
- 'symbol': symbol,
219
- 'decimal_places': decimal_places,
220
- 'is_active': True
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
- except Exception as e:
240
- self.stdout.write(f" ❌ Failed to create {code}: {e}")
241
- logger.error(f"Failed to create currency {code}: {e}")
242
-
243
- self.stdout.write(f"📦 Population complete: {created_count} created, {skipped_count} skipped")
244
- return created_count
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) -> int:
247
- """Update USD exchange rates using django_currency module."""
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
- # Build queryset based on options
252
- queryset = Currency.objects.all()
173
+ # Get currencies to update
174
+ queryset = Currency.objects.filter(is_active=True)
253
175
 
254
176
  if options['currency']:
255
- queryset = queryset.filter(code__iexact=options['currency'])
177
+ currency_code = options['currency'].upper()
178
+ queryset = queryset.filter(code=currency_code)
256
179
  if not queryset.exists():
257
- raise CommandError(f"Currency '{options['currency']}' not found")
180
+ raise CommandError(f"Currency '{currency_code}' not found in database")
258
181
 
259
- if options['currency_type']:
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
- # For now, skip staleness check since rate fields don't exist
266
- # TODO: Implement proper rate tracking fields
267
- pass
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
- # Apply limit
270
- queryset = queryset[:options['limit']]
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
- self.stdout.write(f"📊 Processing {queryset.count()} currencies...")
276
-
277
- for currency in queryset:
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
- if options['dry_run']:
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
- try:
284
- # Use django_currency module to get rate
285
- if currency.code == 'USD':
286
- # USD is the base currency
287
- usd_rate = 1.0
288
- else:
289
- # Get rate from django_currency
290
- usd_rate = get_exchange_rate(currency.code, 'USD')
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
- if usd_rate and usd_rate > 0:
293
- # Update rate in ProviderCurrency (create if doesn't exist)
294
- provider_currency, created = ProviderCurrency.objects.get_or_create(
295
- currency=currency,
296
- provider='system', # System-level rate
297
- provider_currency_code=currency.code,
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
- except (CurrencyError, CurrencyNotFoundError) as e:
318
- self.stdout.write(f" ⚠️ {currency.code}: {str(e)}")
319
- error_count += 1
320
- except Exception as e:
321
- self.stdout.write(f" ❌ {currency.code}: {str(e)}")
322
- error_count += 1
323
- logger.error(f"Failed to update rate for {currency.code}: {e}")
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 _sync_provider_currencies(self, options):
329
- """Sync provider currencies after rate updates."""
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 django.core.management import call_command
261
+ # Get rate from hybrid client
262
+ rate_obj = self.converter.hybrid.fetch_rate(currency.code, 'USD')
335
263
 
336
- if options['provider']:
337
- call_command('manage_providers', '--provider', options['provider'])
338
- else:
339
- call_command('manage_providers', '--all')
340
-
341
- self.stdout.write("🔄 Provider sync completed")
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"⚠️ Provider sync failed: {e}")
345
- logger.warning(f"Provider sync failed: {e}")
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
- try:
351
- total_currencies = Currency.objects.count()
352
- fiat_count = Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count()
353
- crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
354
- active_count = Currency.objects.filter(is_active=True).count()
355
-
356
- # Count currencies with provider configs (simplified since rate fields don't exist)
357
- currencies_with_rates = Currency.objects.filter(
358
- provider_configs__isnull=False
359
- ).distinct().count()
360
-
361
- rate_coverage = (currencies_with_rates / total_currencies * 100) if total_currencies > 0 else 0
362
-
363
- self.stdout.write("\n📊 Final Statistics:")
364
- self.stdout.write(f" Total currencies: {total_currencies}")
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 get_provider_registry
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