django-cfg 1.5.8__py3-none-any.whl → 1.5.14__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 (119) 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/agents/management/commands/create_agent.py +5 -194
  6. django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
  7. django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
  8. django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
  9. django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
  10. django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
  11. django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
  12. django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
  13. django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
  14. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +5 -5
  15. django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
  16. django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
  17. django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
  18. django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
  19. django_cfg/apps/integrations/grpc/admin/config.py +113 -9
  20. django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
  21. django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
  22. django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
  23. django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
  24. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
  25. django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
  26. django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
  27. django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
  28. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +130 -0
  29. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +171 -96
  30. django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
  31. django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
  32. django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
  33. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
  34. django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
  35. django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
  36. django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
  37. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
  38. django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
  39. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
  40. django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
  41. django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
  42. django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
  43. django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
  44. django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
  45. django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
  46. django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
  47. django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
  48. django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
  49. django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
  50. django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
  51. django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
  52. django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
  53. django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
  54. django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
  55. django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
  56. django_cfg/apps/integrations/grpc/urls.py +8 -0
  57. django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
  58. django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
  59. django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
  60. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +177 -0
  61. django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
  62. django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
  63. django_cfg/apps/integrations/grpc/views/charts.py +21 -14
  64. django_cfg/apps/integrations/grpc/views/config.py +8 -6
  65. django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
  66. django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
  67. django_cfg/apps/integrations/grpc/views/services.py +30 -21
  68. django_cfg/apps/integrations/grpc/views/testing.py +45 -43
  69. django_cfg/apps/integrations/rq/views/jobs.py +19 -9
  70. django_cfg/apps/integrations/rq/views/schedule.py +7 -3
  71. django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
  72. django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
  73. django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
  74. django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
  75. django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
  76. django_cfg/config.py +33 -0
  77. django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
  78. django_cfg/management/commands/check_endpoints.py +2 -2
  79. django_cfg/management/commands/check_settings.py +3 -10
  80. django_cfg/management/commands/clear_constance.py +3 -10
  81. django_cfg/management/commands/create_token.py +4 -11
  82. django_cfg/management/commands/list_urls.py +4 -10
  83. django_cfg/management/commands/migrate_all.py +18 -12
  84. django_cfg/management/commands/migrator.py +4 -11
  85. django_cfg/management/commands/script.py +4 -10
  86. django_cfg/management/commands/show_config.py +8 -16
  87. django_cfg/management/commands/show_urls.py +5 -11
  88. django_cfg/management/commands/superuser.py +4 -11
  89. django_cfg/management/commands/tree.py +5 -10
  90. django_cfg/management/utils/README.md +402 -0
  91. django_cfg/management/utils/__init__.py +29 -0
  92. django_cfg/management/utils/mixins.py +176 -0
  93. django_cfg/middleware/pagination.py +53 -54
  94. django_cfg/models/api/grpc/__init__.py +15 -21
  95. django_cfg/models/api/grpc/config.py +155 -73
  96. django_cfg/models/ngrok/config.py +7 -6
  97. django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
  98. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
  99. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
  100. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
  101. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
  102. django_cfg/modules/django_client/core/parser/base.py +114 -30
  103. django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
  104. django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
  105. django_cfg/modules/django_email/management/commands/test_email.py +4 -10
  106. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
  107. django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
  108. django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
  109. django_cfg/modules/django_unfold/navigation.py +6 -18
  110. django_cfg/pyproject.toml +1 -1
  111. django_cfg/registry/modules.py +1 -4
  112. django_cfg/requirements.txt +52 -0
  113. django_cfg/static/frontend/admin.zip +0 -0
  114. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/METADATA +1 -1
  115. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/RECORD +118 -97
  116. django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
  117. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/WHEEL +0 -0
  118. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/entry_points.txt +0 -0
  119. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/licenses/LICENSE +0 -0
@@ -1,22 +1,22 @@
1
1
  """
2
- Django management command to run gRPC server.
2
+ Django management command to run async gRPC server.
3
3
 
4
4
  Usage:
5
5
  python manage.py rungrpc
6
6
  python manage.py rungrpc --host 0.0.0.0 --port 50051
7
- python manage.py rungrpc --workers 20
8
7
  """
9
8
 
10
9
  from __future__ import annotations
11
10
 
12
- import logging
11
+ import asyncio
13
12
  import signal
14
13
  import sys
15
- from concurrent import futures
16
14
 
17
15
  from django.conf import settings
18
16
  from django.core.management.base import BaseCommand
19
17
 
18
+ from django_cfg.modules.django_logging import get_logger
19
+
20
20
  # Check dependencies before importing grpc
21
21
  from django_cfg.apps.integrations.grpc._cfg import check_grpc_dependencies
22
22
 
@@ -28,24 +28,36 @@ except Exception as e:
28
28
 
29
29
  # Now safe to import grpc
30
30
  import grpc
31
-
32
- logger = logging.getLogger(__name__)
31
+ import grpc.aio
33
32
 
34
33
 
35
34
  class Command(BaseCommand):
36
35
  """
37
- Run gRPC server with auto-discovered services.
36
+ Run async gRPC server with auto-discovered services.
38
37
 
39
38
  Features:
39
+ - Async server with grpc.aio
40
40
  - Auto-discovers and registers services
41
- - Configurable host, port, and workers
41
+ - Configurable host, port
42
42
  - Health check support
43
43
  - Reflection support
44
44
  - Graceful shutdown
45
45
  - Signal handling
46
46
  """
47
47
 
48
- help = "Run gRPC server"
48
+ # Web execution metadata
49
+ web_executable = False
50
+ requires_input = False
51
+ is_destructive = False
52
+
53
+ help = "Run async gRPC server"
54
+
55
+ def __init__(self, *args, **kwargs):
56
+ """Initialize with self.logger and async server reference."""
57
+ super().__init__(*args, **kwargs)
58
+ self.logger = get_logger('rungrpc')
59
+ self.server = None
60
+ self.shutdown_event = None
49
61
 
50
62
  def add_arguments(self, parser):
51
63
  """Add command arguments."""
@@ -61,12 +73,6 @@ class Command(BaseCommand):
61
73
  default=None,
62
74
  help="Server port (default: from settings or 50051)",
63
75
  )
64
- parser.add_argument(
65
- "--workers",
66
- type=int,
67
- default=None,
68
- help="Max worker threads (default: from settings or 10)",
69
- )
70
76
  parser.add_argument(
71
77
  "--no-reflection",
72
78
  action="store_true",
@@ -77,53 +83,98 @@ class Command(BaseCommand):
77
83
  action="store_true",
78
84
  help="Disable health check service",
79
85
  )
86
+ parser.add_argument(
87
+ "--asyncio-debug",
88
+ action="store_true",
89
+ help="Enable asyncio debug mode",
90
+ )
80
91
 
81
92
  def handle(self, *args, **options):
82
- """Run gRPC server."""
93
+ """Run async gRPC server."""
94
+ # Enable asyncio debug if requested
95
+ if options.get("asyncio_debug"):
96
+ asyncio.get_event_loop().set_debug(True)
97
+ self.logger.info("Asyncio debug mode enabled")
98
+
99
+ # Run async main
100
+ asyncio.run(self._async_main(*args, **options))
101
+
102
+ async def _async_main(self, *args, **options):
103
+ """Main async server loop."""
83
104
  # Import models here to avoid AppRegistryNotReady
84
105
  from django_cfg.apps.integrations.grpc.models import GRPCServerStatus
106
+ from django_cfg.apps.integrations.grpc.services.config_helper import (
107
+ get_grpc_server_config,
108
+ )
85
109
 
86
110
  # Get configuration
87
- grpc_server_config = getattr(settings, "GRPC_SERVER", {})
88
-
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)
93
-
94
- # Server options
95
- enable_reflection = not options["no_reflection"] and grpc_server_config.get(
96
- "enable_reflection", False
97
- )
98
- enable_health_check = not options["no_health_check"] and grpc_server_config.get(
99
- "enable_health_check", True
100
- )
111
+ grpc_server_config_obj = get_grpc_server_config()
112
+
113
+ # Fallback to settings if not configured via django-cfg
114
+ if not grpc_server_config_obj:
115
+ grpc_server_config = getattr(settings, "GRPC_SERVER", {})
116
+ host = options["host"] or grpc_server_config.get("host", "[::]")
117
+ port = options["port"] or grpc_server_config.get("port", 50051)
118
+ max_concurrent_streams = grpc_server_config.get("max_concurrent_streams", None)
119
+ enable_reflection = not options["no_reflection"] and grpc_server_config.get(
120
+ "enable_reflection", False
121
+ )
122
+ enable_health_check = not options["no_health_check"] and grpc_server_config.get(
123
+ "enable_health_check", True
124
+ )
125
+ else:
126
+ # Use django-cfg config
127
+ host = options["host"] or grpc_server_config_obj.host
128
+ port = options["port"] or grpc_server_config_obj.port
129
+ max_concurrent_streams = grpc_server_config_obj.max_concurrent_streams
130
+ enable_reflection = (
131
+ not options["no_reflection"] and grpc_server_config_obj.enable_reflection
132
+ )
133
+ enable_health_check = (
134
+ not options["no_health_check"]
135
+ and grpc_server_config_obj.enable_health_check
136
+ )
137
+ grpc_server_config = {
138
+ "host": grpc_server_config_obj.host,
139
+ "port": grpc_server_config_obj.port,
140
+ "max_concurrent_streams": grpc_server_config_obj.max_concurrent_streams,
141
+ "enable_reflection": grpc_server_config_obj.enable_reflection,
142
+ "enable_health_check": grpc_server_config_obj.enable_health_check,
143
+ "compression": grpc_server_config_obj.compression,
144
+ "max_send_message_length": grpc_server_config_obj.max_send_message_length,
145
+ "max_receive_message_length": grpc_server_config_obj.max_receive_message_length,
146
+ "keepalive_time_ms": grpc_server_config_obj.keepalive_time_ms,
147
+ "keepalive_timeout_ms": grpc_server_config_obj.keepalive_timeout_ms,
148
+ }
101
149
 
102
150
  # gRPC options
103
151
  grpc_options = self._build_grpc_options(grpc_server_config)
104
152
 
105
- # Create server
106
- server = grpc.server(
107
- futures.ThreadPoolExecutor(max_workers=max_workers),
153
+ # Add max_concurrent_streams if specified
154
+ if max_concurrent_streams:
155
+ grpc_options.append(("grpc.max_concurrent_streams", max_concurrent_streams))
156
+
157
+ # Create async server
158
+ self.server = grpc.aio.server(
108
159
  options=grpc_options,
109
- interceptors=self._build_interceptors(),
160
+ interceptors=await self._build_interceptors_async(),
110
161
  )
111
162
 
112
163
  # Discover and register services FIRST
113
- service_count = self._register_services(server)
164
+ service_count = await self._register_services_async(self.server)
114
165
 
115
166
  # Add health check with registered services
116
167
  health_servicer = None
117
168
  if enable_health_check:
118
- health_servicer = self._add_health_check(server)
169
+ health_servicer = await self._add_health_check_async(self.server)
119
170
 
120
171
  # Add reflection
121
172
  if enable_reflection:
122
- self._add_reflection(server)
173
+ await self._add_reflection_async(self.server)
123
174
 
124
175
  # Bind server
125
176
  address = f"{host}:{port}"
126
- server.add_insecure_port(address)
177
+ self.server.add_insecure_port(address)
127
178
 
128
179
  # Track server status in database
129
180
  server_status = None
@@ -131,35 +182,41 @@ class Command(BaseCommand):
131
182
  import os
132
183
  from django_cfg.apps.integrations.grpc.services import ServiceDiscovery
133
184
 
134
- # Get registered services metadata
185
+ # Get registered services metadata (run in thread to avoid blocking)
135
186
  discovery = ServiceDiscovery()
136
- services_metadata = discovery.get_registered_services()
187
+ services_metadata = await asyncio.to_thread(
188
+ discovery.get_registered_services
189
+ )
137
190
 
138
- server_status = GRPCServerStatus.objects.start_server(
191
+ server_status = await asyncio.to_thread(
192
+ GRPCServerStatus.objects.start_server,
139
193
  host=host,
140
194
  port=port,
141
195
  pid=os.getpid(),
142
- max_workers=max_workers,
196
+ max_workers=0, # Async server - no workers
143
197
  enable_reflection=enable_reflection,
144
198
  enable_health_check=enable_health_check,
145
199
  )
146
200
 
147
201
  # Store registered services in database
148
202
  server_status.registered_services = services_metadata
149
- server_status.save(update_fields=["registered_services"])
203
+ await asyncio.to_thread(
204
+ server_status.save,
205
+ update_fields=["registered_services"]
206
+ )
150
207
 
151
208
  except Exception as e:
152
- logger.warning(f"Could not start server status tracking: {e}")
209
+ self.logger.warning(f"Could not start server status tracking: {e}")
153
210
 
154
211
  # Start server
155
- server.start()
212
+ await self.server.start()
156
213
 
157
214
  # Mark server as running
158
215
  if server_status:
159
216
  try:
160
- server_status.mark_running()
217
+ await asyncio.to_thread(server_status.mark_running)
161
218
  except Exception as e:
162
- logger.warning(f"Could not mark server as running: {e}")
219
+ self.logger.warning(f"Could not mark server as running: {e}")
163
220
 
164
221
  # Display gRPC-specific startup info
165
222
  try:
@@ -168,32 +225,35 @@ class Command(BaseCommand):
168
225
 
169
226
  # Get registered service names
170
227
  discovery = ServiceDiscovery()
171
- services_metadata = discovery.get_registered_services()
228
+ services_metadata = await asyncio.to_thread(
229
+ discovery.get_registered_services
230
+ )
172
231
  service_names = [s.get('name', 'Unknown') for s in services_metadata]
173
232
 
174
233
  # Display startup info
175
234
  grpc_display = GRPCDisplayManager()
176
- grpc_display.display_grpc_startup(
235
+ await asyncio.to_thread(
236
+ grpc_display.display_grpc_startup,
177
237
  host=host,
178
238
  port=port,
179
- max_workers=max_workers,
239
+ max_workers=0, # Async server
180
240
  enable_reflection=enable_reflection,
181
241
  enable_health_check=enable_health_check,
182
242
  registered_services=service_count,
183
243
  service_names=service_names,
184
244
  )
185
245
  except Exception as e:
186
- logger.warning(f"Could not display gRPC startup info: {e}")
246
+ self.logger.warning(f"Could not display gRPC startup info: {e}")
187
247
 
188
248
  # Setup signal handlers for graceful shutdown
189
- self._setup_signal_handlers(server, server_status)
249
+ self._setup_signal_handlers_async(self.server, server_status)
190
250
 
191
251
  # Keep server running
192
- self.stdout.write(self.style.SUCCESS("\n✅ gRPC server is running..."))
252
+ self.stdout.write(self.style.SUCCESS("\n✅ Async gRPC server is running..."))
193
253
  self.stdout.write("Press CTRL+C to stop\n")
194
254
 
195
255
  try:
196
- server.wait_for_termination()
256
+ await self.server.wait_for_termination()
197
257
  except KeyboardInterrupt:
198
258
  # Signal handler will take care of graceful shutdown
199
259
  pass
@@ -244,12 +304,12 @@ class Command(BaseCommand):
244
304
 
245
305
  return options
246
306
 
247
- def _build_interceptors(self) -> list:
307
+ async def _build_interceptors_async(self) -> list:
248
308
  """
249
- Build server interceptors from configuration.
309
+ Build async server interceptors from configuration.
250
310
 
251
311
  Returns:
252
- List of interceptor instances
312
+ List of async interceptor instances
253
313
  """
254
314
  grpc_framework_config = getattr(settings, "GRPC_FRAMEWORK", {})
255
315
  interceptor_paths = grpc_framework_config.get("SERVER_INTERCEPTORS", [])
@@ -269,22 +329,22 @@ class Command(BaseCommand):
269
329
  interceptor = interceptor_class()
270
330
  interceptors.append(interceptor)
271
331
 
272
- logger.debug(f"Loaded interceptor: {class_name}")
332
+ self.logger.debug(f"Loaded async interceptor: {class_name}")
273
333
 
274
334
  except Exception as e:
275
- logger.error(f"Failed to load interceptor {interceptor_path}: {e}")
335
+ self.logger.error(f"Failed to load async interceptor {interceptor_path}: {e}")
276
336
 
277
337
  return interceptors
278
338
 
279
- def _add_health_check(self, server):
339
+ async def _add_health_check_async(self, server):
280
340
  """
281
- Add health check service with per-service status tracking.
341
+ Add health check service to async server.
282
342
 
283
343
  Args:
284
- server: gRPC server instance
344
+ server: Async gRPC server instance
285
345
 
286
346
  Returns:
287
- health_servicer: Health servicer instance (for dynamic updates) or None
347
+ health_servicer: Health servicer instance or None
288
348
  """
289
349
  try:
290
350
  from grpc_health.v1 import health, health_pb2, health_pb2_grpc
@@ -294,14 +354,13 @@ class Command(BaseCommand):
294
354
 
295
355
  # Set overall server status
296
356
  health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)
297
- logger.info("Overall server health: SERVING")
357
+ self.logger.info("Overall server health: SERVING")
298
358
 
299
- # Get registered service names
359
+ # Get registered service names from async server
300
360
  service_names = []
301
361
  if hasattr(server, '_state') and hasattr(server._state, 'generic_handlers'):
302
362
  for handler in server._state.generic_handlers:
303
363
  if hasattr(handler, 'service_name'):
304
- # service_name() returns a callable or list
305
364
  names = handler.service_name()
306
365
  if callable(names):
307
366
  names = names()
@@ -316,44 +375,42 @@ class Command(BaseCommand):
316
375
  service_name,
317
376
  health_pb2.HealthCheckResponse.SERVING
318
377
  )
319
- logger.info(f"Service '{service_name}' health: SERVING")
378
+ self.logger.info(f"Service '{service_name}' health: SERVING")
320
379
 
321
- # Register health service
380
+ # Register health service to async server
322
381
  health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)
323
382
 
324
- logger.info(
383
+ self.logger.info(
325
384
  f"✅ Health check enabled for {len(service_names)} service(s)"
326
385
  )
327
386
 
328
- # Return servicer for dynamic health updates
329
387
  return health_servicer
330
388
 
331
389
  except ImportError:
332
- logger.warning(
390
+ self.logger.warning(
333
391
  "grpcio-health-checking not installed. "
334
392
  "Install with: pip install 'django-cfg[grpc]'"
335
393
  )
336
394
  return None
337
395
  except Exception as e:
338
- logger.error(f"Failed to add health check service: {e}")
396
+ self.logger.error(f"Failed to add health check service: {e}")
339
397
  return None
340
398
 
341
- def _add_reflection(self, server):
399
+ async def _add_reflection_async(self, server):
342
400
  """
343
- Add reflection service to server.
401
+ Add reflection service to async server.
344
402
 
345
403
  Args:
346
- server: gRPC server instance
404
+ server: Async gRPC server instance
347
405
  """
348
406
  try:
349
407
  from grpc_reflection.v1alpha import reflection
350
408
 
351
- # Get service names from registered services
409
+ # Get service names from async server
352
410
  service_names = []
353
411
  if hasattr(server, '_state') and hasattr(server._state, 'generic_handlers'):
354
412
  for handler in server._state.generic_handlers:
355
413
  if hasattr(handler, 'service_name'):
356
- # service_name() returns a callable or list
357
414
  names = handler.service_name()
358
415
  if callable(names):
359
416
  names = names()
@@ -365,25 +422,25 @@ class Command(BaseCommand):
365
422
  # Add grpc.reflection.v1alpha.ServerReflection service itself
366
423
  service_names.append('grpc.reflection.v1alpha.ServerReflection')
367
424
 
368
- # Add reflection
425
+ # Add reflection to async server
369
426
  reflection.enable_server_reflection(service_names, server)
370
427
 
371
- logger.info(f"Server reflection enabled for {len(service_names)} service(s)")
428
+ self.logger.info(f"Server reflection enabled for {len(service_names)} service(s)")
372
429
 
373
430
  except ImportError:
374
- logger.warning(
431
+ self.logger.warning(
375
432
  "grpcio-reflection not installed. "
376
433
  "Install with: pip install grpcio-reflection"
377
434
  )
378
435
  except Exception as e:
379
- logger.error(f"Failed to enable server reflection: {e}")
436
+ self.logger.error(f"Failed to enable server reflection: {e}")
380
437
 
381
- def _register_services(self, server) -> int:
438
+ async def _register_services_async(self, server) -> int:
382
439
  """
383
- Discover and register services to server.
440
+ Discover and register services to async server.
384
441
 
385
442
  Args:
386
- server: gRPC server instance
443
+ server: Async gRPC server instance
387
444
 
388
445
  Returns:
389
446
  Number of services registered
@@ -391,22 +448,26 @@ class Command(BaseCommand):
391
448
  try:
392
449
  from django_cfg.apps.integrations.grpc.services.discovery import discover_and_register_services
393
450
 
394
- count = discover_and_register_services(server)
451
+ # Service registration is sync, run in thread
452
+ count = await asyncio.to_thread(
453
+ discover_and_register_services,
454
+ server
455
+ )
395
456
  return count
396
457
 
397
458
  except Exception as e:
398
- logger.error(f"Failed to register services: {e}", exc_info=True)
459
+ self.logger.error(f"Failed to register services: {e}", exc_info=True)
399
460
  self.stdout.write(
400
461
  self.style.ERROR(f"Error registering services: {e}")
401
462
  )
402
463
  return 0
403
464
 
404
- def _setup_signal_handlers(self, server, server_status=None):
465
+ def _setup_signal_handlers_async(self, server, server_status=None):
405
466
  """
406
- Setup signal handlers for graceful shutdown.
467
+ Setup signal handlers for graceful async server shutdown.
407
468
 
408
469
  Args:
409
- server: gRPC server instance
470
+ server: Async gRPC server instance
410
471
  server_status: GRPCServerStatus instance (optional)
411
472
  """
412
473
  # Flag to prevent multiple shutdown attempts
@@ -423,19 +484,33 @@ class Command(BaseCommand):
423
484
  # Mark server as stopping
424
485
  if server_status:
425
486
  try:
426
- server_status.mark_stopping()
487
+ import django
488
+ if django.VERSION >= (3, 0):
489
+ from asgiref.sync import sync_to_async
490
+ # Run in sync context
491
+ try:
492
+ server_status.mark_stopping()
493
+ except:
494
+ pass
427
495
  except Exception as e:
428
- logger.warning(f"Could not mark server as stopping: {e}")
496
+ self.logger.warning(f"Could not mark server as stopping: {e}")
429
497
 
430
- # Stop server with grace period
431
- server.stop(grace=5)
498
+ # Stop async server
499
+ try:
500
+ # Create task to stop server
501
+ loop = asyncio.get_event_loop()
502
+ loop.create_task(server.stop(grace=5))
503
+ except Exception as e:
504
+ self.logger.error(f"Error stopping server: {e}")
432
505
 
433
- # Mark server as stopped
506
+ # Mark server as stopped (async-safe)
434
507
  if server_status:
435
508
  try:
436
- server_status.mark_stopped()
509
+ from asgiref.sync import sync_to_async
510
+ # Wrap sync DB operation in sync_to_async
511
+ asyncio.create_task(sync_to_async(server_status.mark_stopped)())
437
512
  except Exception as e:
438
- logger.warning(f"Could not mark server as stopped: {e}")
513
+ self.logger.warning(f"Could not mark server as stopped: {e}")
439
514
 
440
515
  self.stdout.write(self.style.SUCCESS("✅ Server stopped"))
441
516
 
@@ -0,0 +1,75 @@
1
+ """
2
+ Django management command for gRPC integration testing.
3
+
4
+ Usage:
5
+ python manage.py test_grpc_integration
6
+ python manage.py test_grpc_integration --app crypto
7
+ python manage.py test_grpc_integration --quiet
8
+ """
9
+
10
+ import sys
11
+
12
+ from django_cfg.management.utils import SafeCommand
13
+
14
+ from django_cfg.apps.integrations.grpc.utils import GRPCIntegrationTest
15
+
16
+
17
+ class Command(SafeCommand):
18
+ command_name = 'test_grpc_integration'
19
+ help = "Run comprehensive gRPC integration test with API keys"
20
+
21
+ def add_arguments(self, parser):
22
+ parser.add_argument(
23
+ "--app",
24
+ type=str,
25
+ default="crypto",
26
+ help="Django app label to test (default: crypto)",
27
+ )
28
+ parser.add_argument(
29
+ "--quiet",
30
+ "-q",
31
+ action="store_true",
32
+ help="Suppress verbose output",
33
+ )
34
+
35
+ def handle(self, *args, **options):
36
+ app_label = options["app"]
37
+ quiet = options["quiet"]
38
+
39
+ if not quiet:
40
+ self.stdout.write(
41
+ self.style.SUCCESS(f"Starting gRPC integration test for app: {app_label}")
42
+ )
43
+
44
+ # Создаем и запускаем тест
45
+ test = GRPCIntegrationTest(app_label=app_label, quiet=quiet)
46
+
47
+ try:
48
+ success = test.run()
49
+
50
+ if success:
51
+ self.stdout.write(
52
+ self.style.SUCCESS("\n✅ Integration test completed successfully!")
53
+ )
54
+ sys.exit(0)
55
+ else:
56
+ self.stdout.write(
57
+ self.style.ERROR("\n❌ Integration test failed!")
58
+ )
59
+ sys.exit(1)
60
+
61
+ except KeyboardInterrupt:
62
+ self.stdout.write(
63
+ self.style.WARNING("\n⚠️ Test interrupted by user")
64
+ )
65
+ test.step6_cleanup()
66
+ sys.exit(1)
67
+
68
+ except Exception as e:
69
+ self.stdout.write(
70
+ self.style.ERROR(f"\n❌ Critical error: {e}")
71
+ )
72
+ import traceback
73
+ traceback.print_exc()
74
+ test.step6_cleanup()
75
+ sys.exit(1)
@@ -2,10 +2,12 @@
2
2
  Managers for gRPC app models.
3
3
  """
4
4
 
5
+ from .grpc_api_key import GrpcApiKeyManager
5
6
  from .grpc_request_log import GRPCRequestLogManager, GRPCRequestLogQuerySet
6
7
  from .grpc_server_status import GRPCServerStatusManager
7
8
 
8
9
  __all__ = [
10
+ "GrpcApiKeyManager",
9
11
  "GRPCRequestLogManager",
10
12
  "GRPCRequestLogQuerySet",
11
13
  "GRPCServerStatusManager",