django-cfg 1.5.8__py3-none-any.whl → 1.5.20__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.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (159) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/commands/serializers.py +152 -0
  3. django_cfg/apps/api/commands/views.py +32 -0
  4. django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
  5. django_cfg/apps/business/accounts/serializers/profile.py +42 -0
  6. django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
  7. django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
  8. django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
  9. django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
  10. django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
  11. django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
  12. django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
  13. django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
  14. django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
  15. django_cfg/apps/business/support/serializers.py +3 -2
  16. django_cfg/apps/integrations/centrifugo/apps.py +2 -1
  17. django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
  18. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
  19. django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
  20. django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
  21. django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
  22. django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
  23. django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
  24. django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
  25. django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
  26. django_cfg/apps/integrations/centrifugo/urls.py +8 -0
  27. django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
  28. django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
  29. django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
  30. django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
  31. django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
  32. django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
  33. django_cfg/apps/integrations/grpc/admin/config.py +113 -9
  34. django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
  35. django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
  36. django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
  37. django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
  38. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
  39. django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
  40. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
  41. django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
  42. django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
  43. django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
  44. django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
  45. django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
  46. django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
  47. django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
  48. django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
  49. django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
  50. django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
  51. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
  52. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
  53. django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
  54. django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
  55. django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
  56. django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
  57. django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
  58. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
  59. django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
  60. django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
  61. django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
  62. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
  63. django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
  64. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
  65. django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
  66. django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
  67. django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
  68. django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
  69. django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
  70. django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
  71. django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
  72. django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
  73. django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
  74. django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
  75. django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
  76. django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
  77. django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
  78. django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
  79. django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
  80. django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
  81. django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
  82. django_cfg/apps/integrations/grpc/urls.py +8 -0
  83. django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
  84. django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
  85. django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
  86. django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
  87. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
  88. django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
  89. django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
  90. django_cfg/apps/integrations/grpc/views/charts.py +21 -14
  91. django_cfg/apps/integrations/grpc/views/config.py +8 -6
  92. django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
  93. django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
  94. django_cfg/apps/integrations/grpc/views/services.py +30 -21
  95. django_cfg/apps/integrations/grpc/views/testing.py +45 -43
  96. django_cfg/apps/integrations/rq/views/jobs.py +19 -9
  97. django_cfg/apps/integrations/rq/views/schedule.py +7 -3
  98. django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
  99. django_cfg/apps/system/dashboard/serializers/config.py +95 -9
  100. django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
  101. django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
  102. django_cfg/apps/system/frontend/views.py +87 -6
  103. django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
  104. django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
  105. django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
  106. django_cfg/config.py +33 -0
  107. django_cfg/core/builders/security_builder.py +1 -0
  108. django_cfg/core/generation/integration_generators/api.py +2 -0
  109. django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
  110. django_cfg/management/commands/check_endpoints.py +2 -2
  111. django_cfg/management/commands/check_settings.py +3 -10
  112. django_cfg/management/commands/clear_constance.py +3 -10
  113. django_cfg/management/commands/create_token.py +4 -11
  114. django_cfg/management/commands/list_urls.py +4 -10
  115. django_cfg/management/commands/migrate_all.py +18 -12
  116. django_cfg/management/commands/migrator.py +4 -11
  117. django_cfg/management/commands/script.py +4 -10
  118. django_cfg/management/commands/show_config.py +8 -16
  119. django_cfg/management/commands/show_urls.py +5 -11
  120. django_cfg/management/commands/superuser.py +4 -11
  121. django_cfg/management/commands/tree.py +5 -10
  122. django_cfg/management/utils/README.md +402 -0
  123. django_cfg/management/utils/__init__.py +29 -0
  124. django_cfg/management/utils/mixins.py +176 -0
  125. django_cfg/middleware/pagination.py +53 -54
  126. django_cfg/models/api/grpc/__init__.py +15 -21
  127. django_cfg/models/api/grpc/config.py +155 -73
  128. django_cfg/models/ngrok/config.py +7 -6
  129. django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
  130. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
  131. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
  132. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
  133. django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
  134. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
  135. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
  136. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
  137. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
  138. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
  139. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
  140. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
  141. django_cfg/modules/django_client/core/ir/schema.py +15 -1
  142. django_cfg/modules/django_client/core/parser/base.py +126 -30
  143. django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
  144. django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
  145. django_cfg/modules/django_email/management/commands/test_email.py +4 -10
  146. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
  147. django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
  148. django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
  149. django_cfg/modules/django_unfold/navigation.py +6 -18
  150. django_cfg/pyproject.toml +1 -1
  151. django_cfg/registry/modules.py +1 -4
  152. django_cfg/requirements.txt +52 -0
  153. django_cfg/static/frontend/admin.zip +0 -0
  154. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
  155. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
  156. django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
  157. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
  158. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
  159. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,205 @@
1
+ """
2
+ Management command to load pre-built agent templates.
3
+ """
4
+
5
+ import asyncio
6
+
7
+ from django.contrib.auth.models import User
8
+ from django.core.management.base import CommandError
9
+
10
+ from django_cfg.management.utils import AdminCommand
11
+
12
+ from django_cfg.apps.business.agents.models.registry import AgentDefinition
13
+
14
+
15
+ class Command(AdminCommand):
16
+ """Load agent definitions from templates."""
17
+
18
+ command_name = 'load_agent_templates'
19
+ help = 'Load pre-built agent templates'
20
+
21
+ def add_arguments(self, parser):
22
+ """Add command arguments."""
23
+ parser.add_argument(
24
+ '--list',
25
+ action='store_true',
26
+ help='List available templates'
27
+ )
28
+ parser.add_argument(
29
+ '--load',
30
+ type=str,
31
+ nargs='*',
32
+ help='Load specific templates (space-separated names)'
33
+ )
34
+ parser.add_argument(
35
+ '--load-all',
36
+ action='store_true',
37
+ help='Load all available templates'
38
+ )
39
+ parser.add_argument(
40
+ '--creator',
41
+ type=str,
42
+ help='Username of agent creator (defaults to first superuser)'
43
+ )
44
+
45
+ def handle(self, *args, **options):
46
+ """Handle command execution."""
47
+ if options['list']:
48
+ self._list_templates()
49
+ elif options['load'] or options['load_all']:
50
+ asyncio.run(self._load_templates(options))
51
+ else:
52
+ self.stdout.write(
53
+ self.style.ERROR('Please specify --list, --load, or --load-all')
54
+ )
55
+
56
+ def _list_templates(self):
57
+ """List available templates."""
58
+ templates = self._get_available_templates()
59
+
60
+ self.stdout.write(self.style.SUCCESS('📋 Available Agent Templates:'))
61
+ self.stdout.write('=' * 40)
62
+
63
+ for category, agents in templates.items():
64
+ self.stdout.write(f"\n{category.upper()}:")
65
+ for agent_name, agent_info in agents.items():
66
+ self.stdout.write(f" • {agent_name}: {agent_info['description']}")
67
+
68
+ async def _load_templates(self, options):
69
+ """Load templates."""
70
+ creator = await self._get_creator_user(options.get('creator'))
71
+ templates = self._get_available_templates()
72
+
73
+ if options['load_all']:
74
+ # Load all templates
75
+ to_load = []
76
+ for category_templates in templates.values():
77
+ to_load.extend(category_templates.keys())
78
+ else:
79
+ to_load = options['load']
80
+
81
+ loaded_count = 0
82
+
83
+ for template_name in to_load:
84
+ # Find template
85
+ template_info = None
86
+ for category_templates in templates.values():
87
+ if template_name in category_templates:
88
+ template_info = category_templates[template_name]
89
+ break
90
+
91
+ if not template_info:
92
+ self.stdout.write(
93
+ self.style.WARNING(f"Template '{template_name}' not found")
94
+ )
95
+ continue
96
+
97
+ # Check if agent already exists
98
+ if await AgentDefinition.objects.filter(name=template_name).aexists():
99
+ self.stdout.write(
100
+ self.style.WARNING(f"Agent '{template_name}' already exists, skipping")
101
+ )
102
+ continue
103
+
104
+ # Create agent
105
+ try:
106
+ agent_data = template_info.copy()
107
+ agent_data['name'] = template_name
108
+ agent_data['created_by'] = creator
109
+
110
+ await AgentDefinition.objects.acreate(**agent_data)
111
+
112
+ self.stdout.write(
113
+ self.style.SUCCESS(f"✅ Loaded template: {template_name}")
114
+ )
115
+ loaded_count += 1
116
+
117
+ except Exception as e:
118
+ self.stdout.write(
119
+ self.style.ERROR(f"Failed to load template '{template_name}': {e}")
120
+ )
121
+
122
+ self.stdout.write(
123
+ self.style.SUCCESS(f"\n🎉 Loaded {loaded_count} agent templates")
124
+ )
125
+
126
+ def _get_available_templates(self):
127
+ """Get available agent templates."""
128
+ return {
129
+ 'content': {
130
+ 'content_analyzer': {
131
+ 'description': 'Analyze content sentiment, topics, and quality',
132
+ 'instructions': 'Analyze content for sentiment, topics, keywords, and quality metrics.',
133
+ 'deps_type': 'ContentDeps',
134
+ 'output_type': 'AnalysisResult',
135
+ 'category': 'content',
136
+ 'model': 'openai:gpt-4o-mini',
137
+ },
138
+ 'content_generator': {
139
+ 'description': 'Generate high-quality content based on requirements',
140
+ 'instructions': 'Generate engaging, well-structured content based on type, audience, and style requirements.',
141
+ 'deps_type': 'ContentDeps',
142
+ 'output_type': 'ProcessResult',
143
+ 'category': 'content',
144
+ 'model': 'openai:gpt-4o-mini',
145
+ },
146
+ 'content_validator': {
147
+ 'description': 'Validate content quality and compliance',
148
+ 'instructions': 'Validate content for grammar, style, accuracy, and guideline compliance.',
149
+ 'deps_type': 'ContentDeps',
150
+ 'output_type': 'ValidationResult',
151
+ 'category': 'content',
152
+ 'model': 'openai:gpt-4o-mini',
153
+ },
154
+ },
155
+ 'data': {
156
+ 'data_processor': {
157
+ 'description': 'Process and transform data',
158
+ 'instructions': 'Process, clean, and transform data according to specifications.',
159
+ 'deps_type': 'DataProcessingDeps',
160
+ 'output_type': 'ProcessResult',
161
+ 'category': 'data',
162
+ 'model': 'openai:gpt-4o-mini',
163
+ },
164
+ 'data_validator': {
165
+ 'description': 'Validate data quality and integrity',
166
+ 'instructions': 'Validate data quality, check for errors, and ensure integrity.',
167
+ 'deps_type': 'DataProcessingDeps',
168
+ 'output_type': 'ValidationResult',
169
+ 'category': 'data',
170
+ 'model': 'openai:gpt-4o-mini',
171
+ },
172
+ },
173
+ 'business': {
174
+ 'business_rules': {
175
+ 'description': 'Apply business rules and logic',
176
+ 'instructions': 'Apply business rules, validate decisions, and ensure compliance.',
177
+ 'deps_type': 'BusinessLogicDeps',
178
+ 'output_type': 'ProcessResult',
179
+ 'category': 'business',
180
+ 'model': 'openai:gpt-4o-mini',
181
+ },
182
+ 'decision_maker': {
183
+ 'description': 'Make decisions based on criteria',
184
+ 'instructions': 'Analyze options and make informed decisions based on criteria and context.',
185
+ 'deps_type': 'BusinessLogicDeps',
186
+ 'output_type': 'ProcessResult',
187
+ 'category': 'business',
188
+ 'model': 'openai:gpt-4o-mini',
189
+ },
190
+ }
191
+ }
192
+
193
+ async def _get_creator_user(self, username):
194
+ """Get creator user."""
195
+ if username:
196
+ try:
197
+ return await User.objects.aget(username=username)
198
+ except User.DoesNotExist:
199
+ raise CommandError(f"User '{username}' not found")
200
+ else:
201
+ # Use first superuser
202
+ try:
203
+ return await User.objects.filter(is_superuser=True).afirst()
204
+ except User.DoesNotExist:
205
+ raise CommandError("No superuser found. Please create a superuser first or specify --creator")
@@ -5,17 +5,19 @@ Management command to show orchestrator status.
5
5
  import asyncio
6
6
  from datetime import timedelta
7
7
 
8
- from django.core.management.base import BaseCommand
9
8
  from django.utils import timezone
10
9
 
10
+ from django_cfg.management.utils import SafeCommand
11
+
11
12
  from django_cfg.apps.business.agents.integration.registry import get_registry
12
13
  from django_cfg.apps.business.agents.models.execution import AgentExecution, WorkflowExecution
13
14
  from django_cfg.apps.business.agents.models.registry import AgentDefinition
14
15
 
15
16
 
16
- class Command(BaseCommand):
17
+ class Command(SafeCommand):
17
18
  """Show Django Orchestrator status and statistics."""
18
19
 
20
+ command_name = 'orchestrator_status'
19
21
  help = 'Display Django Orchestrator status and statistics'
20
22
 
21
23
  def add_arguments(self, parser):
@@ -3,17 +3,19 @@ Knowledge Base statistics command.
3
3
  """
4
4
 
5
5
  from django.contrib.auth import get_user_model
6
- from django.core.management.base import BaseCommand
7
6
  from django.db import models
8
7
  from django.db.models import Avg, Count, ExpressionWrapper, F, Q, Sum
9
8
  from django.db.models.functions import Extract
10
9
 
10
+ from django_cfg.management.utils import SafeCommand
11
+
11
12
  User = get_user_model()
12
13
 
13
14
 
14
- class Command(BaseCommand):
15
+ class Command(SafeCommand):
15
16
  """Display Knowledge Base statistics."""
16
17
 
18
+ command_name = 'knowbase_stats'
17
19
  help = 'Display Knowledge Base usage statistics'
18
20
 
19
21
  def add_arguments(self, parser):
@@ -2,13 +2,15 @@
2
2
  Setup command for Knowledge Base application.
3
3
  """
4
4
 
5
- from django.core.management.base import BaseCommand
6
5
  from django.db import connection
7
6
 
7
+ from django_cfg.management.utils import AdminCommand
8
8
 
9
- class Command(BaseCommand):
9
+
10
+ class Command(AdminCommand):
10
11
  """Setup Knowledge Base with pgvector extension and initial data."""
11
12
 
13
+ command_name = 'setup_knowbase'
12
14
  help = 'Setup Knowledge Base with pgvector extension and run migrations'
13
15
 
14
16
  def add_arguments(self, parser):
@@ -3,7 +3,9 @@ Management command to test newsletter sending functionality.
3
3
  """
4
4
 
5
5
  from django.contrib.auth import get_user_model
6
- from django.core.management.base import BaseCommand, CommandError
6
+ from django.core.management.base import CommandError
7
+
8
+ from django_cfg.management.utils import SafeCommand
7
9
 
8
10
  from django_cfg.apps.business.newsletter.models import Newsletter, NewsletterCampaign, NewsletterSubscription
9
11
  from django_cfg.apps.business.newsletter.services.email_service import NewsletterEmailService
@@ -11,7 +13,8 @@ from django_cfg.apps.business.newsletter.services.email_service import Newslette
11
13
  User = get_user_model()
12
14
 
13
15
 
14
- class Command(BaseCommand):
16
+ class Command(SafeCommand):
17
+ command_name = 'test_newsletter'
15
18
  help = 'Test newsletter sending functionality'
16
19
 
17
20
  def add_arguments(self, parser):
@@ -7,11 +7,12 @@ Uses questionary for interactive selection or accepts payment ID as argument.
7
7
  from uuid import UUID
8
8
 
9
9
  import questionary
10
- from django.core.management.base import BaseCommand
11
10
  from rich.console import Console
12
11
  from rich.panel import Panel
13
12
  from rich.table import Table
14
13
 
14
+ from django_cfg.management.utils import InteractiveCommand
15
+
15
16
  from django_cfg.apps.business.payments.models import Payment
16
17
  from django_cfg.apps.business.payments.services import PaymentService, CheckStatusRequest
17
18
  from django_cfg.apps.business.payments.api.views import get_nowpayments_provider
@@ -19,7 +20,8 @@ from django_cfg.apps.business.payments.api.views import get_nowpayments_provider
19
20
  console = Console()
20
21
 
21
22
 
22
- class Command(BaseCommand):
23
+ class Command(InteractiveCommand):
24
+ command_name = 'check_payment_status'
23
25
  help = 'Check payment status interactively or by payment ID'
24
26
 
25
27
  def add_arguments(self, parser):
@@ -8,11 +8,12 @@ from decimal import Decimal, InvalidOperation
8
8
 
9
9
  import questionary
10
10
  from django.contrib.auth import get_user_model
11
- from django.core.management.base import BaseCommand
12
11
  from rich.console import Console
13
12
  from rich.panel import Panel
14
13
  from rich.table import Table
15
14
 
15
+ from django_cfg.management.utils import InteractiveCommand
16
+
16
17
  from django_cfg.apps.business.payments.models import Currency, Payment
17
18
  from django_cfg.apps.business.payments.services import PaymentService, CreatePaymentRequest
18
19
  from django_cfg.apps.business.payments.api.views import get_nowpayments_provider
@@ -21,7 +22,8 @@ User = get_user_model()
21
22
  console = Console()
22
23
 
23
24
 
24
- class Command(BaseCommand):
25
+ class Command(InteractiveCommand):
26
+ command_name = 'create_payment'
25
27
  help = 'Create a payment interactively using questionary wizard'
26
28
 
27
29
  def add_arguments(self, parser):
@@ -5,19 +5,21 @@ Fetches available currencies from NowPayments and updates local database.
5
5
  """
6
6
 
7
7
  import questionary
8
- from django.core.management.base import BaseCommand
9
8
  from rich.console import Console
10
9
  from rich.panel import Panel
11
10
  from rich.progress import Progress, SpinnerColumn, TextColumn
12
11
  from rich.table import Table
13
12
 
13
+ from django_cfg.management.utils import AdminCommand
14
+
14
15
  from django_cfg.apps.business.payments.models import Currency
15
16
  from django_cfg.apps.business.payments.api.views import get_nowpayments_provider
16
17
 
17
18
  console = Console()
18
19
 
19
20
 
20
- class Command(BaseCommand):
21
+ class Command(AdminCommand):
22
+ command_name = 'sync_currencies'
21
23
  help = 'Sync currencies from NowPayments provider'
22
24
 
23
25
  def add_arguments(self, parser):
@@ -8,13 +8,14 @@ from .models import Message, Ticket
8
8
  User = get_user_model()
9
9
 
10
10
  class SenderSerializer(serializers.ModelSerializer):
11
- avatar = serializers.SerializerMethodField()
11
+ avatar = serializers.SerializerMethodField(allow_null=True)
12
12
  initials = serializers.ReadOnlyField()
13
13
 
14
14
  class Meta:
15
15
  model = User
16
16
  fields = ['id', 'display_username', 'email', 'avatar', 'initials', 'is_staff', 'is_superuser']
17
- read_only_fields = fields
17
+ # Don't include avatar in read_only_fields to make it optional in OpenAPI schema
18
+ read_only_fields = ['id', 'display_username', 'email', 'initials', 'is_staff', 'is_superuser']
18
19
 
19
20
  def get_avatar(self, obj) -> Optional[str]:
20
21
  if obj.avatar:
@@ -32,6 +32,7 @@ class CentrifugoConfig(AppConfig):
32
32
  Initialize app when Django starts.
33
33
 
34
34
  Validates that all required Centrifugo dependencies are installed.
35
+ Registers signal handlers for JWT token customization.
35
36
  """
36
37
  from django_cfg.modules.django_logging import get_logger
37
38
 
@@ -40,7 +41,7 @@ class CentrifugoConfig(AppConfig):
40
41
  # Check dependencies if needed (only when using Centrifugo features)
41
42
  self._check_dependencies_if_needed()
42
43
 
43
- logger.info("Centrifugo app initialized")
44
+ logger.info("Centrifugo app initialized (middleware will inject JWT tokens)")
44
45
 
45
46
  def _check_dependencies_if_needed(self):
46
47
  """
@@ -10,6 +10,7 @@ import { Centrifuge } from 'centrifuge';
10
10
  export class CentrifugoRPCClient {
11
11
  private centrifuge: Centrifuge;
12
12
  private subscription: any;
13
+ private channelSubscriptions: Map<string, any> = new Map();
13
14
  private pendingRequests: Map<string, { resolve: Function; reject: Function }> = new Map();
14
15
  private readonly replyChannel: string;
15
16
  private readonly timeout: number;
@@ -27,12 +28,7 @@ export class CentrifugoRPCClient {
27
28
  token,
28
29
  });
29
30
 
30
- this.centrifuge.on('connected', (ctx) => {
31
- console.log('✅ Connected to Centrifugo');
32
- });
33
-
34
31
  this.centrifuge.on('disconnected', (ctx) => {
35
- console.warn('Disconnected:', ctx);
36
32
  // Reject all pending requests
37
33
  this.pendingRequests.forEach(({ reject }) => {
38
34
  reject(new Error('Disconnected from Centrifugo'));
@@ -43,6 +39,27 @@ export class CentrifugoRPCClient {
43
39
 
44
40
  async connect(): Promise<void> {
45
41
  return new Promise((resolve, reject) => {
42
+ let resolved = false;
43
+
44
+ // Listen to Centrifuge connection events
45
+ const onConnected = () => {
46
+ if (!resolved) {
47
+ resolved = true;
48
+ resolve();
49
+ }
50
+ };
51
+
52
+ const onError = (ctx: any) => {
53
+ if (!resolved) {
54
+ resolved = true;
55
+ reject(new Error(ctx.message || 'Connection error'));
56
+ }
57
+ };
58
+
59
+ this.centrifuge.on('connected', onConnected);
60
+ this.centrifuge.on('error', onError);
61
+
62
+ // Start connection
46
63
  this.centrifuge.connect();
47
64
 
48
65
  // Subscribe to reply channel
@@ -53,11 +70,17 @@ export class CentrifugoRPCClient {
53
70
  });
54
71
 
55
72
  this.subscription.on('subscribed', () => {
56
- resolve();
73
+ // Subscription successful (optional, we already resolved on 'connected')
57
74
  });
58
75
 
59
76
  this.subscription.on('error', (ctx: any) => {
60
- reject(new Error(ctx.error?.message || 'Subscription error'));
77
+ // Error code 105 = "already subscribed" (server-side subscription from JWT)
78
+ // This is not an error - the channel is already active via server-side subscription
79
+ if (ctx.error?.code === 105) {
80
+ // This is fine, server-side subscription exists
81
+ } else {
82
+ console.error(`Subscription error for ${this.replyChannel}:`, ctx.error);
83
+ }
61
84
  });
62
85
 
63
86
  this.subscription.subscribe();
@@ -65,9 +88,14 @@ export class CentrifugoRPCClient {
65
88
  }
66
89
 
67
90
  async disconnect(): Promise<void> {
91
+ // Unsubscribe from all event channels
92
+ this.unsubscribeAll();
93
+
94
+ // Unsubscribe from RPC reply channel
68
95
  if (this.subscription) {
69
96
  this.subscription.unsubscribe();
70
97
  }
98
+
71
99
  this.centrifuge.disconnect();
72
100
  }
73
101
 
@@ -103,21 +131,17 @@ export class CentrifugoRPCClient {
103
131
  // Publish request
104
132
  await this.centrifuge.publish('rpc.requests', message);
105
133
 
106
- console.log(`📤 RPC call: ${method} (${correlationId})`);
107
-
108
134
  return promise;
109
135
  }
110
136
 
111
137
  private handleResponse(data: any): void {
112
138
  const correlationId = data.correlation_id;
113
139
  if (!correlationId) {
114
- console.warn('Received response without correlation_id');
115
140
  return;
116
141
  }
117
142
 
118
143
  const pending = this.pendingRequests.get(correlationId);
119
144
  if (!pending) {
120
- console.warn(`Received response for unknown correlation_id: ${correlationId}`);
121
145
  return;
122
146
  }
123
147
 
@@ -126,7 +150,6 @@ export class CentrifugoRPCClient {
126
150
  if (data.error) {
127
151
  pending.reject(new Error(data.error.message || 'RPC error'));
128
152
  } else {
129
- console.log(`📥 RPC response: ${correlationId}`);
130
153
  pending.resolve(data.result);
131
154
  }
132
155
  }
@@ -134,4 +157,120 @@ export class CentrifugoRPCClient {
134
157
  private generateCorrelationId(): string {
135
158
  return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
136
159
  }
160
+
161
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
162
+ // Channel Subscription API (for gRPC events)
163
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
164
+
165
+ /**
166
+ * Subscribe to a Centrifugo channel for real-time events.
167
+ *
168
+ * @param channel - Channel name (e.g., 'bot#bot-123#heartbeat')
169
+ * @param callback - Callback for received messages
170
+ * @returns Unsubscribe function
171
+ *
172
+ * @example
173
+ * const unsubscribe = client.subscribe('bot#bot-123#heartbeat', (data) => {
174
+ * console.log('Heartbeat:', data);
175
+ * });
176
+ *
177
+ * // Later: unsubscribe when done
178
+ * unsubscribe();
179
+ */
180
+ subscribe(channel: string, callback: (data: any) => void): () => void {
181
+ // Check if already subscribed
182
+ if (this.channelSubscriptions.has(channel)) {
183
+ return () => {}; // Return no-op unsubscribe
184
+ }
185
+
186
+ // Create new subscription
187
+ const sub = this.centrifuge.newSubscription(channel);
188
+
189
+ // Handle publications
190
+ sub.on('publication', (ctx: any) => {
191
+ callback(ctx.data);
192
+ });
193
+
194
+ // Handle subscription lifecycle
195
+ sub.on('subscribed', () => {
196
+ // Subscription successful
197
+ });
198
+
199
+ sub.on('error', (ctx: any) => {
200
+ console.error(`Subscription error for ${channel}:`, ctx.error);
201
+ });
202
+
203
+ // Start subscription
204
+ sub.subscribe();
205
+
206
+ // Store subscription
207
+ this.channelSubscriptions.set(channel, sub);
208
+
209
+ // Return unsubscribe function
210
+ return () => this.unsubscribe(channel);
211
+ }
212
+
213
+ /**
214
+ * Unsubscribe from a channel.
215
+ *
216
+ * @param channel - Channel name
217
+ */
218
+ unsubscribe(channel: string): void {
219
+ const sub = this.channelSubscriptions.get(channel);
220
+ if (!sub) {
221
+ return;
222
+ }
223
+
224
+ sub.unsubscribe();
225
+ this.channelSubscriptions.delete(channel);
226
+ }
227
+
228
+ /**
229
+ * Unsubscribe from all channels.
230
+ */
231
+ unsubscribeAll(): void {
232
+ if (this.channelSubscriptions.size === 0) {
233
+ return;
234
+ }
235
+
236
+ this.channelSubscriptions.forEach((sub, channel) => {
237
+ sub.unsubscribe();
238
+ });
239
+ this.channelSubscriptions.clear();
240
+ }
241
+
242
+ /**
243
+ * Get list of active client-side subscriptions.
244
+ */
245
+ getActiveSubscriptions(): string[] {
246
+ return Array.from(this.channelSubscriptions.keys());
247
+ }
248
+
249
+ /**
250
+ * Get list of server-side subscriptions (from JWT token).
251
+ *
252
+ * These are channels automatically subscribed by Centrifugo server
253
+ * based on the 'channels' claim in the JWT token.
254
+ */
255
+ getServerSideSubscriptions(): string[] {
256
+ try {
257
+ // Access Centrifuge.js internal state for server-side subs
258
+ // @ts-ignore - accessing internal property
259
+ const serverSubs = this.centrifuge._serverSubs || {};
260
+ return Object.keys(serverSubs);
261
+ } catch (error) {
262
+ return [];
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Get all active subscriptions (both client-side and server-side).
268
+ */
269
+ getAllSubscriptions(): string[] {
270
+ const clientSubs = this.getActiveSubscriptions();
271
+ const serverSubs = this.getServerSideSubscriptions();
272
+
273
+ // Combine and deduplicate
274
+ return Array.from(new Set([...clientSubs, ...serverSubs]));
275
+ }
137
276
  }
@@ -10,21 +10,22 @@ import logging
10
10
  from pathlib import Path
11
11
  from typing import List
12
12
 
13
- from django.core.management.base import BaseCommand, CommandError
13
+ from django.core.management.base import CommandError
14
14
  from django.utils.termcolors import colorize
15
15
 
16
+ from django_cfg.management.utils import AdminCommand
17
+
16
18
  from django_cfg.apps.integrations.centrifugo.codegen.discovery import discover_rpc_methods_from_router
17
19
  from django_cfg.apps.integrations.centrifugo.codegen.generators.python_thin import PythonThinGenerator
18
20
  from django_cfg.apps.integrations.centrifugo.codegen.generators.typescript_thin import TypeScriptThinGenerator
19
21
  from django_cfg.apps.integrations.centrifugo.codegen.generators.go_thin import GoThinGenerator
20
22
  from django_cfg.apps.integrations.centrifugo.router import get_global_router
21
23
 
22
- logger = logging.getLogger(__name__)
23
-
24
24
 
25
- class Command(BaseCommand):
25
+ class Command(AdminCommand):
26
26
  """Generate type-safe client SDKs for Centrifugo WebSocket RPC."""
27
27
 
28
+ command_name = 'generate_centrifugo_clients'
28
29
  help = "Generate type-safe client SDKs for Centrifugo WebSocket RPC from @websocket_rpc handlers"
29
30
 
30
31
  def add_arguments(self, parser):
@@ -131,11 +132,10 @@ class Command(BaseCommand):
131
132
  if not methods:
132
133
  self.stdout.write(
133
134
  colorize(
134
- "No RPC methods found. Did you register handlers with @websocket_rpc?",
135
+ "⚠️ No RPC methods found. Will generate base RPC client without API methods.",
135
136
  fg="yellow",
136
137
  )
137
138
  )
138
- return
139
139
 
140
140
  # Create output directory
141
141
  output_dir.mkdir(parents=True, exist_ok=True)