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
@@ -1,21 +1,53 @@
1
1
  """
2
- Django management command to run gRPC server.
2
+ Django management command to run async gRPC server with auto-reload support.
3
3
 
4
4
  Usage:
5
+ # Development mode (with auto-reload)
5
6
  python manage.py rungrpc
7
+
8
+ # Production mode (no auto-reload)
9
+ python manage.py rungrpc --noreload
10
+
11
+ # Custom host and port
6
12
  python manage.py rungrpc --host 0.0.0.0 --port 50051
7
- python manage.py rungrpc --workers 20
13
+
14
+ # With Centrifugo test event on startup
15
+ python manage.py rungrpc --test
16
+
17
+ # Disable specific features
18
+ python manage.py rungrpc --no-reflection --no-health-check
19
+
20
+ Auto-reload behavior:
21
+ - Enabled by default in development mode (ENV_MODE != "production")
22
+ - Disabled by default in production mode (ENV_MODE == "production")
23
+ - Use --noreload to explicitly disable auto-reload
24
+ - Server will restart automatically when Python files change
25
+
26
+ Test mode:
27
+ - Use --test to send a test Centrifugo event on server startup
28
+ - Useful for verifying Centrifugo integration is working
29
+ - Event published to: grpc#rungrpc#startup#test
8
30
  """
9
31
 
10
32
  from __future__ import annotations
11
33
 
34
+ import asyncio
12
35
  import logging
13
36
  import signal
14
37
  import sys
15
- from concurrent import futures
38
+ import threading
16
39
 
17
40
  from django.conf import settings
18
41
  from django.core.management.base import BaseCommand
42
+ from django.utils import autoreload
43
+
44
+ from django_cfg.core.config import get_current_config
45
+ from django_cfg.modules.django_logging import get_logger
46
+ from django_cfg.apps.integrations.grpc.utils.streaming_logger import (
47
+ setup_streaming_logger,
48
+ log_server_start,
49
+ log_server_shutdown,
50
+ )
19
51
 
20
52
  # Check dependencies before importing grpc
21
53
  from django_cfg.apps.integrations.grpc._cfg import check_grpc_dependencies
@@ -28,24 +60,46 @@ except Exception as e:
28
60
 
29
61
  # Now safe to import grpc
30
62
  import grpc
31
-
32
- logger = logging.getLogger(__name__)
63
+ import grpc.aio
33
64
 
34
65
 
35
66
  class Command(BaseCommand):
36
67
  """
37
- Run gRPC server with auto-discovered services.
68
+ Run async gRPC server with auto-discovered services and hot-reload.
38
69
 
39
70
  Features:
71
+ - Async server with grpc.aio
40
72
  - Auto-discovers and registers services
41
- - Configurable host, port, and workers
73
+ - Hot-reload in development mode (watches for file changes)
74
+ - Configurable host, port
42
75
  - Health check support
43
76
  - Reflection support
44
77
  - Graceful shutdown
45
78
  - Signal handling
79
+
80
+ Hot-reload:
81
+ - Automatically enabled in development mode (ENV_MODE != "production")
82
+ - Automatically disabled in production mode (ENV_MODE == "production")
83
+ - Use --noreload to explicitly disable in development
84
+ - Works like Django's runserver - restarts on code changes
46
85
  """
47
86
 
48
- help = "Run gRPC server"
87
+ # Web execution metadata
88
+ web_executable = False
89
+ requires_input = False
90
+ is_destructive = False
91
+
92
+ help = "Run async gRPC server with optional hot-reload support"
93
+
94
+ def __init__(self, *args, **kwargs):
95
+ """Initialize with self.logger and async server reference."""
96
+ super().__init__(*args, **kwargs)
97
+ self.logger = get_logger('rungrpc')
98
+ self.streaming_logger = None # Will be initialized when server starts
99
+ self.server = None
100
+ self.shutdown_event = None
101
+ self.server_status = None
102
+ self.server_config = None # Store config for re-registration
49
103
 
50
104
  def add_arguments(self, parser):
51
105
  """Add command arguments."""
@@ -61,12 +115,6 @@ class Command(BaseCommand):
61
115
  default=None,
62
116
  help="Server port (default: from settings or 50051)",
63
117
  )
64
- parser.add_argument(
65
- "--workers",
66
- type=int,
67
- default=None,
68
- help="Max worker threads (default: from settings or 10)",
69
- )
70
118
  parser.add_argument(
71
119
  "--no-reflection",
72
120
  action="store_true",
@@ -77,53 +125,190 @@ class Command(BaseCommand):
77
125
  action="store_true",
78
126
  help="Disable health check service",
79
127
  )
128
+ parser.add_argument(
129
+ "--asyncio-debug",
130
+ action="store_true",
131
+ help="Enable asyncio debug mode",
132
+ )
133
+ parser.add_argument(
134
+ "--noreload",
135
+ action="store_false",
136
+ dest="use_reloader",
137
+ help="Disable auto-reloader (default: enabled in dev mode)",
138
+ )
139
+ parser.add_argument(
140
+ "--test",
141
+ action="store_true",
142
+ help="Send test Centrifugo event on startup (for testing integration)",
143
+ )
80
144
 
81
145
  def handle(self, *args, **options):
82
- """Run gRPC server."""
83
- # Import models here to avoid AppRegistryNotReady
84
- from django_cfg.apps.integrations.grpc.models import GRPCServerStatus
146
+ """Run async gRPC server with optional auto-reload."""
147
+ config = get_current_config()
148
+
149
+ # Determine if we should use auto-reload
150
+ # Changed default to False for stability with bidirectional streaming
151
+ use_reloader = options.get("use_reloader", False)
152
+
153
+ # Check if we're in production mode (disable reloader in production)
154
+ if config and hasattr(config, 'is_production'):
155
+ is_production = config.is_production # property, not method
156
+ else:
157
+ # Fallback to settings
158
+ env_mode = getattr(settings, "ENV_MODE", "development").lower()
159
+ is_production = env_mode == "production"
160
+
161
+ # Disable reloader in production by default
162
+ if is_production and options.get("use_reloader") is None:
163
+ use_reloader = False
164
+ self.stdout.write(
165
+ self.style.WARNING(
166
+ "Production mode - auto-reloader disabled"
167
+ )
168
+ )
85
169
 
86
- # Get configuration
87
- grpc_server_config = getattr(settings, "GRPC_SERVER", {})
170
+ # Enable asyncio debug if requested
171
+ if options.get("asyncio_debug"):
172
+ asyncio.get_event_loop().set_debug(True)
173
+ self.logger.info("Asyncio debug mode enabled")
174
+
175
+ # Run with or without reloader
176
+ if use_reloader:
177
+ from datetime import datetime
178
+ current_time = datetime.now().strftime('%H:%M:%S')
179
+
180
+ self.stdout.write(
181
+ self.style.SUCCESS(
182
+ f"🔄 Auto-reloader enabled - watching for file changes [{current_time}]"
183
+ )
184
+ )
185
+ self.stdout.write(
186
+ self.style.WARNING(
187
+ "⚠️ Note: Active streaming connections will be dropped on reload. Use --noreload for stable bots."
188
+ )
189
+ )
190
+
191
+ # Setup autoreload to watch project directory
192
+ from django.utils.autoreload import autoreload_started
193
+
194
+ def watch_project_files(sender, **kwargs):
195
+ """Automatically watch Django project directory for changes."""
196
+ base_dir = getattr(settings, 'BASE_DIR', None)
197
+ if base_dir:
198
+ sender.watch_dir(str(base_dir), '*.py')
199
+ self.logger.debug(f"Watching project directory: {base_dir}")
88
200
 
89
- # Get server parameters
90
- host = options["host"] or grpc_server_config.get("host", "[::]")
91
- port = options["port"] or grpc_server_config.get("port", 50051)
92
- max_workers = options["workers"] or grpc_server_config.get("max_workers", 10)
201
+ autoreload_started.connect(watch_project_files)
202
+
203
+ # Use autoreload to restart on code changes
204
+ autoreload.run_with_reloader(
205
+ lambda: asyncio.run(self._async_main(*args, **options))
206
+ )
207
+ else:
208
+ # Run directly without reloader
209
+ asyncio.run(self._async_main(*args, **options))
210
+
211
+ async def _async_main(self, *args, **options):
212
+ """Main async server loop."""
213
+ # Setup streaming logger for detailed gRPC logging
214
+ self.streaming_logger = setup_streaming_logger(
215
+ name='grpc_rungrpc',
216
+ level=logging.DEBUG,
217
+ console_level=logging.INFO
218
+ )
93
219
 
94
- # Server options
95
- enable_reflection = not options["no_reflection"] and grpc_server_config.get(
96
- "enable_reflection", False
220
+ config = get_current_config()
221
+ use_reloader = options.get("use_reloader", True)
222
+
223
+ # Determine production mode
224
+ if config and hasattr(config, 'is_production'):
225
+ is_production = config.is_production # property, not method
226
+ else:
227
+ env_mode = getattr(settings, "ENV_MODE", "development").lower()
228
+ is_production = env_mode == "production"
229
+
230
+ # Log startup using reusable function
231
+ start_time = log_server_start(
232
+ self.streaming_logger,
233
+ server_type="gRPC Server",
234
+ mode="Production" if is_production else "Development",
235
+ hotreload_enabled=use_reloader
97
236
  )
98
- enable_health_check = not options["no_health_check"] and grpc_server_config.get(
99
- "enable_health_check", True
237
+
238
+ # Import models here to avoid AppRegistryNotReady
239
+ from django_cfg.apps.integrations.grpc.models import GRPCServerStatus
240
+ from django_cfg.apps.integrations.grpc.services.config_helper import (
241
+ get_grpc_server_config,
100
242
  )
101
243
 
244
+ # Get configuration
245
+ grpc_server_config_obj = get_grpc_server_config()
246
+
247
+ # Fallback to settings if not configured via django-cfg
248
+ if not grpc_server_config_obj:
249
+ grpc_server_config = getattr(settings, "GRPC_SERVER", {})
250
+ host = options["host"] or grpc_server_config.get("host", "[::]")
251
+ port = options["port"] or grpc_server_config.get("port", 50051)
252
+ max_concurrent_streams = grpc_server_config.get("max_concurrent_streams", None)
253
+ enable_reflection = not options["no_reflection"] and grpc_server_config.get(
254
+ "enable_reflection", False
255
+ )
256
+ enable_health_check = not options["no_health_check"] and grpc_server_config.get(
257
+ "enable_health_check", True
258
+ )
259
+ else:
260
+ # Use django-cfg config
261
+ host = options["host"] or grpc_server_config_obj.host
262
+ port = options["port"] or grpc_server_config_obj.port
263
+ max_concurrent_streams = grpc_server_config_obj.max_concurrent_streams
264
+ enable_reflection = (
265
+ not options["no_reflection"] and grpc_server_config_obj.enable_reflection
266
+ )
267
+ enable_health_check = (
268
+ not options["no_health_check"]
269
+ and grpc_server_config_obj.enable_health_check
270
+ )
271
+ grpc_server_config = {
272
+ "host": grpc_server_config_obj.host,
273
+ "port": grpc_server_config_obj.port,
274
+ "max_concurrent_streams": grpc_server_config_obj.max_concurrent_streams,
275
+ "enable_reflection": grpc_server_config_obj.enable_reflection,
276
+ "enable_health_check": grpc_server_config_obj.enable_health_check,
277
+ "compression": grpc_server_config_obj.compression,
278
+ "max_send_message_length": grpc_server_config_obj.max_send_message_length,
279
+ "max_receive_message_length": grpc_server_config_obj.max_receive_message_length,
280
+ "keepalive_time_ms": grpc_server_config_obj.keepalive_time_ms,
281
+ "keepalive_timeout_ms": grpc_server_config_obj.keepalive_timeout_ms,
282
+ }
283
+
102
284
  # gRPC options
103
285
  grpc_options = self._build_grpc_options(grpc_server_config)
104
286
 
105
- # Create server
106
- server = grpc.server(
107
- futures.ThreadPoolExecutor(max_workers=max_workers),
287
+ # Add max_concurrent_streams if specified
288
+ if max_concurrent_streams:
289
+ grpc_options.append(("grpc.max_concurrent_streams", max_concurrent_streams))
290
+
291
+ # Create async server
292
+ self.server = grpc.aio.server(
108
293
  options=grpc_options,
109
- interceptors=self._build_interceptors(),
294
+ interceptors=await self._build_interceptors_async(),
110
295
  )
111
296
 
112
297
  # Discover and register services FIRST
113
- service_count = self._register_services(server)
298
+ service_count = await self._register_services_async(self.server)
114
299
 
115
300
  # Add health check with registered services
116
301
  health_servicer = None
117
302
  if enable_health_check:
118
- health_servicer = self._add_health_check(server)
303
+ health_servicer = await self._add_health_check_async(self.server)
119
304
 
120
305
  # Add reflection
121
306
  if enable_reflection:
122
- self._add_reflection(server)
307
+ await self._add_reflection_async(self.server)
123
308
 
124
309
  # Bind server
125
310
  address = f"{host}:{port}"
126
- server.add_insecure_port(address)
311
+ self.server.add_insecure_port(address)
127
312
 
128
313
  # Track server status in database
129
314
  server_status = None
@@ -131,35 +316,62 @@ class Command(BaseCommand):
131
316
  import os
132
317
  from django_cfg.apps.integrations.grpc.services import ServiceDiscovery
133
318
 
134
- # Get registered services metadata
319
+ # Store config for re-registration
320
+ self.server_config = {
321
+ 'host': host,
322
+ 'port': port,
323
+ 'pid': os.getpid(),
324
+ 'max_workers': 0,
325
+ 'enable_reflection': enable_reflection,
326
+ 'enable_health_check': enable_health_check,
327
+ }
328
+
329
+ # Get registered services metadata (run in thread to avoid blocking)
135
330
  discovery = ServiceDiscovery()
136
- services_metadata = discovery.get_registered_services()
331
+ services_metadata = await asyncio.to_thread(
332
+ discovery.get_registered_services
333
+ )
137
334
 
138
- server_status = GRPCServerStatus.objects.start_server(
335
+ server_status = await asyncio.to_thread(
336
+ GRPCServerStatus.objects.start_server,
139
337
  host=host,
140
338
  port=port,
141
339
  pid=os.getpid(),
142
- max_workers=max_workers,
340
+ max_workers=0, # Async server - no workers
143
341
  enable_reflection=enable_reflection,
144
342
  enable_health_check=enable_health_check,
145
343
  )
146
344
 
147
345
  # Store registered services in database
148
346
  server_status.registered_services = services_metadata
149
- server_status.save(update_fields=["registered_services"])
347
+ await asyncio.to_thread(
348
+ server_status.save,
349
+ update_fields=["registered_services"]
350
+ )
351
+
352
+ # Store in instance for heartbeat
353
+ self.server_status = server_status
150
354
 
151
355
  except Exception as e:
152
- logger.warning(f"Could not start server status tracking: {e}")
356
+ self.logger.warning(f"Could not start server status tracking: {e}")
153
357
 
154
358
  # Start server
155
- server.start()
359
+ await self.server.start()
156
360
 
157
361
  # Mark server as running
158
362
  if server_status:
159
363
  try:
160
- server_status.mark_running()
364
+ await asyncio.to_thread(server_status.mark_running)
161
365
  except Exception as e:
162
- logger.warning(f"Could not mark server as running: {e}")
366
+ self.logger.warning(f"Could not mark server as running: {e}")
367
+
368
+ # Start heartbeat background task
369
+ heartbeat_task = None
370
+ if server_status:
371
+ heartbeat_task = asyncio.create_task(
372
+ self._heartbeat_loop(interval=30)
373
+ )
374
+ self.logger.info("Started heartbeat background task (30s interval)")
163
375
 
164
376
  # Display gRPC-specific startup info
165
377
  try:
@@ -168,35 +380,100 @@ class Command(BaseCommand):
168
380
 
169
381
  # Get registered service names
170
382
  discovery = ServiceDiscovery()
171
- services_metadata = discovery.get_registered_services()
383
+ services_metadata = await asyncio.to_thread(
384
+ discovery.get_registered_services
385
+ )
172
386
  service_names = [s.get('name', 'Unknown') for s in services_metadata]
173
387
 
174
388
  # Display startup info
175
389
  grpc_display = GRPCDisplayManager()
176
- grpc_display.display_grpc_startup(
390
+ await asyncio.to_thread(
391
+ grpc_display.display_grpc_startup,
177
392
  host=host,
178
393
  port=port,
179
- max_workers=max_workers,
394
+ max_workers=0, # Async server
180
395
  enable_reflection=enable_reflection,
181
396
  enable_health_check=enable_health_check,
182
397
  registered_services=service_count,
183
398
  service_names=service_names,
184
399
  )
185
400
  except Exception as e:
186
- logger.warning(f"Could not display gRPC startup info: {e}")
401
+ self.logger.warning(f"Could not display gRPC startup info: {e}")
187
402
 
188
403
  # Setup signal handlers for graceful shutdown
189
- self._setup_signal_handlers(server, server_status)
404
+ self._setup_signal_handlers_async(self.server, server_status)
190
405
 
191
406
  # Keep server running
192
- self.stdout.write(self.style.SUCCESS("\n✅ gRPC server is running..."))
407
+ self.stdout.write(self.style.SUCCESS("\n✅ Async gRPC server is running..."))
408
+
409
+ # Show reloader status
410
+ if use_reloader and not is_production:
411
+ self.stdout.write(
412
+ self.style.SUCCESS(
413
+ "🔄 Auto-reloader active - server will restart on code changes"
414
+ )
415
+ )
416
+
193
417
  self.stdout.write("Press CTRL+C to stop\n")
194
418
 
419
+ # Log server ready
420
+ self.streaming_logger.info("✅ Server ready and accepting connections")
421
+ if use_reloader:
422
+ self.streaming_logger.info("🔄 Watching for file changes...")
423
+
424
+ # Send test Centrifugo event if --test flag is set
425
+ if options.get("test"):
426
+ self.streaming_logger.info("🧪 Sending test Centrifugo event...")
427
+ try:
428
+ from django_cfg.apps.integrations.grpc.centrifugo.demo import send_demo_event
429
+
430
+ test_result = await send_demo_event(
431
+ channel="grpc#rungrpc#startup#test",
432
+ metadata={
433
+ "source": "rungrpc",
434
+ "action": "startup_test",
435
+ "mode": "Development" if not is_production else "Production",
436
+ "host": host,
437
+ "port": port,
438
+ }
439
+ )
440
+
441
+ if test_result:
442
+ self.streaming_logger.info("✅ Test Centrifugo event sent successfully")
443
+ self.stdout.write(self.style.SUCCESS("🧪 Test event published to Centrifugo"))
444
+ else:
445
+ self.streaming_logger.warning("⚠️ Test Centrifugo event failed")
446
+ self.stdout.write(self.style.WARNING("⚠️ Test event failed (check Centrifugo config)"))
447
+
448
+ except Exception as e:
449
+ self.streaming_logger.error(f"❌ Failed to send test event: {e}")
450
+ self.stdout.write(
451
+ self.style.ERROR(f"❌ Test event error: {e}")
452
+ )
453
+
454
+ shutdown_reason = "Unknown"
195
455
  try:
196
- server.wait_for_termination()
456
+ await self.server.wait_for_termination()
457
+ shutdown_reason = "Normal termination"
197
458
  except KeyboardInterrupt:
198
- # Signal handler will take care of graceful shutdown
459
+ shutdown_reason = "Keyboard interrupt"
199
460
  pass
461
+ finally:
462
+ # Log shutdown using reusable function
463
+ log_server_shutdown(
464
+ self.streaming_logger,
465
+ start_time,
466
+ server_type="gRPC Server",
467
+ reason=shutdown_reason
468
+ )
469
+
470
+ # Cancel heartbeat task
471
+ if heartbeat_task and not heartbeat_task.done():
472
+ heartbeat_task.cancel()
473
+ try:
474
+ await heartbeat_task
475
+ except asyncio.CancelledError:
476
+ pass
200
477
 
201
478
  def _build_grpc_options(self, config: dict) -> list:
202
479
  """
@@ -244,12 +521,12 @@ class Command(BaseCommand):
244
521
 
245
522
  return options
246
523
 
247
- def _build_interceptors(self) -> list:
524
+ async def _build_interceptors_async(self) -> list:
248
525
  """
249
- Build server interceptors from configuration.
526
+ Build async server interceptors from configuration.
250
527
 
251
528
  Returns:
252
- List of interceptor instances
529
+ List of async interceptor instances
253
530
  """
254
531
  grpc_framework_config = getattr(settings, "GRPC_FRAMEWORK", {})
255
532
  interceptor_paths = grpc_framework_config.get("SERVER_INTERCEPTORS", [])
@@ -269,22 +546,22 @@ class Command(BaseCommand):
269
546
  interceptor = interceptor_class()
270
547
  interceptors.append(interceptor)
271
548
 
272
- logger.debug(f"Loaded interceptor: {class_name}")
549
+ self.logger.debug(f"Loaded async interceptor: {class_name}")
273
550
 
274
551
  except Exception as e:
275
- logger.error(f"Failed to load interceptor {interceptor_path}: {e}")
552
+ self.logger.error(f"Failed to load async interceptor {interceptor_path}: {e}")
276
553
 
277
554
  return interceptors
278
555
 
279
- def _add_health_check(self, server):
556
+ async def _add_health_check_async(self, server):
280
557
  """
281
- Add health check service with per-service status tracking.
558
+ Add health check service to async server.
282
559
 
283
560
  Args:
284
- server: gRPC server instance
561
+ server: Async gRPC server instance
285
562
 
286
563
  Returns:
287
- health_servicer: Health servicer instance (for dynamic updates) or None
564
+ health_servicer: Health servicer instance or None
288
565
  """
289
566
  try:
290
567
  from grpc_health.v1 import health, health_pb2, health_pb2_grpc
@@ -294,14 +571,13 @@ class Command(BaseCommand):
294
571
 
295
572
  # Set overall server status
296
573
  health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)
297
- logger.info("Overall server health: SERVING")
574
+ self.logger.info("Overall server health: SERVING")
298
575
 
299
- # Get registered service names
576
+ # Get registered service names from async server
300
577
  service_names = []
301
578
  if hasattr(server, '_state') and hasattr(server._state, 'generic_handlers'):
302
579
  for handler in server._state.generic_handlers:
303
580
  if hasattr(handler, 'service_name'):
304
- # service_name() returns a callable or list
305
581
  names = handler.service_name()
306
582
  if callable(names):
307
583
  names = names()
@@ -316,44 +592,42 @@ class Command(BaseCommand):
316
592
  service_name,
317
593
  health_pb2.HealthCheckResponse.SERVING
318
594
  )
319
- logger.info(f"Service '{service_name}' health: SERVING")
595
+ self.logger.info(f"Service '{service_name}' health: SERVING")
320
596
 
321
- # Register health service
597
+ # Register health service to async server
322
598
  health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)
323
599
 
324
- logger.info(
600
+ self.logger.info(
325
601
  f"✅ Health check enabled for {len(service_names)} service(s)"
326
602
  )
327
603
 
328
- # Return servicer for dynamic health updates
329
604
  return health_servicer
330
605
 
331
606
  except ImportError:
332
- logger.warning(
607
+ self.logger.warning(
333
608
  "grpcio-health-checking not installed. "
334
609
  "Install with: pip install 'django-cfg[grpc]'"
335
610
  )
336
611
  return None
337
612
  except Exception as e:
338
- logger.error(f"Failed to add health check service: {e}")
613
+ self.logger.error(f"Failed to add health check service: {e}")
339
614
  return None
340
615
 
341
- def _add_reflection(self, server):
616
+ async def _add_reflection_async(self, server):
342
617
  """
343
- Add reflection service to server.
618
+ Add reflection service to async server.
344
619
 
345
620
  Args:
346
- server: gRPC server instance
621
+ server: Async gRPC server instance
347
622
  """
348
623
  try:
349
624
  from grpc_reflection.v1alpha import reflection
350
625
 
351
- # Get service names from registered services
626
+ # Get service names from async server
352
627
  service_names = []
353
628
  if hasattr(server, '_state') and hasattr(server._state, 'generic_handlers'):
354
629
  for handler in server._state.generic_handlers:
355
630
  if hasattr(handler, 'service_name'):
356
- # service_name() returns a callable or list
357
631
  names = handler.service_name()
358
632
  if callable(names):
359
633
  names = names()
@@ -365,25 +639,25 @@ class Command(BaseCommand):
365
639
  # Add grpc.reflection.v1alpha.ServerReflection service itself
366
640
  service_names.append('grpc.reflection.v1alpha.ServerReflection')
367
641
 
368
- # Add reflection
642
+ # Add reflection to async server
369
643
  reflection.enable_server_reflection(service_names, server)
370
644
 
371
- logger.info(f"Server reflection enabled for {len(service_names)} service(s)")
645
+ self.logger.info(f"Server reflection enabled for {len(service_names)} service(s)")
372
646
 
373
647
  except ImportError:
374
- logger.warning(
648
+ self.logger.warning(
375
649
  "grpcio-reflection not installed. "
376
650
  "Install with: pip install grpcio-reflection"
377
651
  )
378
652
  except Exception as e:
379
- logger.error(f"Failed to enable server reflection: {e}")
653
+ self.logger.error(f"Failed to enable server reflection: {e}")
380
654
 
381
- def _register_services(self, server) -> int:
655
+ async def _register_services_async(self, server) -> int:
382
656
  """
383
- Discover and register services to server.
657
+ Discover and register services to async server.
384
658
 
385
659
  Args:
386
- server: gRPC server instance
660
+ server: Async gRPC server instance
387
661
 
388
662
  Returns:
389
663
  Number of services registered
@@ -391,24 +665,115 @@ class Command(BaseCommand):
391
665
  try:
392
666
  from django_cfg.apps.integrations.grpc.services.discovery import discover_and_register_services
393
667
 
394
- count = discover_and_register_services(server)
668
+ # Service registration is sync, run in thread
669
+ count = await asyncio.to_thread(
670
+ discover_and_register_services,
671
+ server
672
+ )
395
673
  return count
396
674
 
397
675
  except Exception as e:
398
- logger.error(f"Failed to register services: {e}", exc_info=True)
676
+ self.logger.error(f"Failed to register services: {e}", exc_info=True)
399
677
  self.stdout.write(
400
678
  self.style.ERROR(f"Error registering services: {e}")
401
679
  )
402
680
  return 0
403
681
 
404
- def _setup_signal_handlers(self, server, server_status=None):
682
+ async def _heartbeat_loop(self, interval: int = 30):
683
+ """
684
+ Periodically update server heartbeat with auto-recovery.
685
+
686
+ If server record is deleted from database, automatically re-registers
687
+ the server to maintain monitoring continuity.
688
+
689
+ Args:
690
+ interval: Heartbeat interval in seconds (default: 30)
691
+ """
692
+ from django_cfg.apps.integrations.grpc.models import GRPCServerStatus
693
+ from asgiref.sync import sync_to_async
694
+
695
+ try:
696
+ while True:
697
+ await asyncio.sleep(interval)
698
+
699
+ if not self.server_status or not self.server_config:
700
+ self.logger.warning("No server status or config available")
701
+ continue
702
+
703
+ try:
704
+ # Check if record still exists
705
+ record_exists = await sync_to_async(
706
+ GRPCServerStatus.objects.filter(
707
+ id=self.server_status.id
708
+ ).exists
709
+ )()
710
+
711
+ if not record_exists:
712
+ # Record was deleted - re-register server
713
+ self.logger.warning(
714
+ "Server record was deleted from database, "
715
+ "re-registering..."
716
+ )
717
+
718
+ # Get services metadata for re-registration
719
+ from django_cfg.apps.integrations.grpc.services import ServiceDiscovery
720
+ discovery = ServiceDiscovery()
721
+ services_metadata = await asyncio.to_thread(
722
+ discovery.get_registered_services
723
+ )
724
+
725
+ # Re-register server
726
+ new_server_status = await asyncio.to_thread(
727
+ GRPCServerStatus.objects.start_server,
728
+ **self.server_config
729
+ )
730
+
731
+ # Store registered services
732
+ new_server_status.registered_services = services_metadata
733
+ await asyncio.to_thread(
734
+ new_server_status.save,
735
+ update_fields=["registered_services"]
736
+ )
737
+
738
+ # Mark as running
739
+ await asyncio.to_thread(new_server_status.mark_running)
740
+
741
+ # Update reference
742
+ self.server_status = new_server_status
743
+
744
+ self.logger.warning(
745
+ f"✅ Successfully re-registered server (ID: {new_server_status.id})"
746
+ )
747
+ else:
748
+ # Record exists - just update heartbeat
749
+ await asyncio.to_thread(self.server_status.mark_running)
750
+ self.logger.debug(f"Heartbeat updated (interval: {interval}s)")
751
+
752
+ except Exception as e:
753
+ self.logger.warning(f"Failed to update heartbeat: {e}")
754
+
755
+ except asyncio.CancelledError:
756
+ self.logger.info("Heartbeat task cancelled")
757
+ raise
758
+
759
+ def _setup_signal_handlers_async(self, server, server_status=None):
405
760
  """
406
- Setup signal handlers for graceful shutdown.
761
+ Setup signal handlers for graceful async server shutdown.
407
762
 
408
763
  Args:
409
- server: gRPC server instance
764
+ server: Async gRPC server instance
410
765
  server_status: GRPCServerStatus instance (optional)
766
+
767
+ Note:
768
+ Signal handlers can only be set in the main thread.
769
+ When running with autoreload, we're in a separate thread,
770
+ so we skip signal handler setup.
411
771
  """
772
+ # Check if we're in the main thread
773
+ if threading.current_thread() is not threading.main_thread():
774
+ # In autoreload mode, Django handles signals - we don't need to set them up
775
+ return
776
+
412
777
  # Flag to prevent multiple shutdown attempts
413
778
  shutdown_initiated = {'value': False}
414
779
 
@@ -423,19 +788,33 @@ class Command(BaseCommand):
423
788
  # Mark server as stopping
424
789
  if server_status:
425
790
  try:
426
- server_status.mark_stopping()
791
+ import django
792
+ if django.VERSION >= (3, 0):
793
+ from asgiref.sync import sync_to_async
794
+ # Run in sync context
795
+ try:
796
+ server_status.mark_stopping()
797
+ except:
798
+ pass
427
799
  except Exception as e:
428
- logger.warning(f"Could not mark server as stopping: {e}")
800
+ self.logger.warning(f"Could not mark server as stopping: {e}")
429
801
 
430
- # Stop server with grace period
431
- server.stop(grace=5)
802
+ # Stop async server
803
+ try:
804
+ # Create task to stop server
805
+ loop = asyncio.get_event_loop()
806
+ loop.create_task(server.stop(grace=5))
807
+ except Exception as e:
808
+ self.logger.error(f"Error stopping server: {e}")
432
809
 
433
- # Mark server as stopped
810
+ # Mark server as stopped (async-safe)
434
811
  if server_status:
435
812
  try:
436
- server_status.mark_stopped()
813
+ from asgiref.sync import sync_to_async
814
+ # Wrap sync DB operation in sync_to_async
815
+ asyncio.create_task(sync_to_async(server_status.mark_stopped)())
437
816
  except Exception as e:
438
- logger.warning(f"Could not mark server as stopped: {e}")
817
+ self.logger.warning(f"Could not mark server as stopped: {e}")
439
818
 
440
819
  self.stdout.write(self.style.SUCCESS("✅ Server stopped"))
441
820