django-cfg 1.5.20__py3-none-any.whl → 1.5.31__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 (98) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
  3. django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
  4. django_cfg/apps/integrations/centrifugo/services/logging.py +90 -14
  5. django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
  6. django_cfg/apps/integrations/centrifugo/views/testing_api.py +47 -43
  7. django_cfg/apps/integrations/centrifugo/views/wrapper.py +41 -29
  8. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
  9. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +1 -1
  10. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +22 -36
  11. django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
  12. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
  13. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
  14. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
  15. django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
  16. django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
  17. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/demo.py +1 -1
  18. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/test_publish.py +4 -4
  19. django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
  20. django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
  21. django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
  22. django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
  23. django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
  24. django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
  25. django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
  26. django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
  27. django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
  28. django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
  29. django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
  30. django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
  31. django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
  32. django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +62 -55
  33. django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +216 -5
  34. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
  35. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
  36. django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
  37. django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
  38. django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
  39. django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
  40. django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
  41. django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
  42. django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
  43. django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
  44. django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
  45. django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
  46. django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
  47. django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
  48. django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
  49. django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
  50. django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
  51. django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
  52. django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
  53. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +55 -8
  54. django_cfg/apps/integrations/grpc/views/charts.py +1 -1
  55. django_cfg/apps/integrations/grpc/views/config.py +1 -1
  56. django_cfg/core/base/config_model.py +11 -0
  57. django_cfg/core/builders/middleware_builder.py +5 -0
  58. django_cfg/management/commands/pool_status.py +153 -0
  59. django_cfg/middleware/pool_cleanup.py +261 -0
  60. django_cfg/models/api/grpc/config.py +2 -2
  61. django_cfg/models/infrastructure/database/config.py +16 -0
  62. django_cfg/models/infrastructure/database/converters.py +2 -0
  63. django_cfg/modules/django_admin/utils/html/composition.py +57 -13
  64. django_cfg/modules/django_admin/utils/html_builder.py +1 -0
  65. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +12 -0
  66. django_cfg/modules/django_client/core/generator/typescript/generator.py +8 -0
  67. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +22 -0
  68. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +4 -0
  69. django_cfg/modules/django_client/core/generator/typescript/templates/utils/validation-events.ts.jinja +133 -0
  70. django_cfg/modules/django_client/core/groups/manager.py +25 -18
  71. django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
  72. django_cfg/modules/django_client/urls.py +38 -5
  73. django_cfg/modules/django_logging/django_logger.py +58 -19
  74. django_cfg/modules/django_twilio/email_otp.py +3 -1
  75. django_cfg/modules/django_twilio/sms.py +3 -1
  76. django_cfg/modules/django_twilio/unified.py +6 -2
  77. django_cfg/modules/django_twilio/whatsapp.py +3 -1
  78. django_cfg/pyproject.toml +3 -3
  79. django_cfg/static/frontend/admin.zip +0 -0
  80. django_cfg/templates/admin/index.html +17 -57
  81. django_cfg/utils/pool_monitor.py +320 -0
  82. django_cfg/utils/smart_defaults.py +233 -7
  83. {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/METADATA +75 -5
  84. {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/RECORD +97 -68
  85. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
  86. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
  87. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
  88. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
  89. /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
  90. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
  91. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
  92. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
  93. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
  94. /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
  95. /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
  96. {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/WHEEL +0 -0
  97. {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/entry_points.txt +0 -0
  98. {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/licenses/LICENSE +0 -0
@@ -237,7 +237,7 @@ class Command(BaseCommand):
237
237
 
238
238
  # Import models here to avoid AppRegistryNotReady
239
239
  from django_cfg.apps.integrations.grpc.models import GRPCServerStatus
240
- from django_cfg.apps.integrations.grpc.services.config_helper import (
240
+ from django_cfg.apps.integrations.grpc.services.management.config_helper import (
241
241
  get_grpc_server_config,
242
242
  )
243
243
 
@@ -332,8 +332,7 @@ class Command(BaseCommand):
332
332
  discovery.get_registered_services
333
333
  )
334
334
 
335
- server_status = await asyncio.to_thread(
336
- GRPCServerStatus.objects.start_server,
335
+ server_status = await GRPCServerStatus.objects.astart_server(
337
336
  host=host,
338
337
  port=port,
339
338
  pid=os.getpid(),
@@ -344,8 +343,7 @@ class Command(BaseCommand):
344
343
 
345
344
  # Store registered services in database
346
345
  server_status.registered_services = services_metadata
347
- await asyncio.to_thread(
348
- server_status.save,
346
+ await server_status.asave(
349
347
  update_fields=["registered_services"]
350
348
  )
351
349
 
@@ -361,7 +359,7 @@ class Command(BaseCommand):
361
359
  # Mark server as running
362
360
  if server_status:
363
361
  try:
364
- await asyncio.to_thread(server_status.mark_running)
362
+ await server_status.amark_running()
365
363
  except Exception as e:
366
364
  self.logger.warning(f"Could not mark server as running: {e}")
367
365
 
@@ -425,7 +423,7 @@ class Command(BaseCommand):
425
423
  if options.get("test"):
426
424
  self.streaming_logger.info("🧪 Sending test Centrifugo event...")
427
425
  try:
428
- from django_cfg.apps.integrations.grpc.centrifugo.demo import send_demo_event
426
+ from django_cfg.apps.integrations.grpc.services.centrifugo.demo import send_demo_event
429
427
 
430
428
  test_result = await send_demo_event(
431
429
  channel="grpc#rungrpc#startup#test",
@@ -690,7 +688,6 @@ class Command(BaseCommand):
690
688
  interval: Heartbeat interval in seconds (default: 30)
691
689
  """
692
690
  from django_cfg.apps.integrations.grpc.models import GRPCServerStatus
693
- from asgiref.sync import sync_to_async
694
691
 
695
692
  try:
696
693
  while True:
@@ -701,12 +698,10 @@ class Command(BaseCommand):
701
698
  continue
702
699
 
703
700
  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
- )()
701
+ # Check if record still exists (Django 5.2: Native async ORM)
702
+ record_exists = await GRPCServerStatus.objects.filter(
703
+ id=self.server_status.id
704
+ ).aexists()
710
705
 
711
706
  if not record_exists:
712
707
  # Record was deleted - re-register server
@@ -722,21 +717,19 @@ class Command(BaseCommand):
722
717
  discovery.get_registered_services
723
718
  )
724
719
 
725
- # Re-register server
726
- new_server_status = await asyncio.to_thread(
727
- GRPCServerStatus.objects.start_server,
720
+ # Re-register server (Django 5.2: Native async ORM)
721
+ new_server_status = await GRPCServerStatus.objects.astart_server(
728
722
  **self.server_config
729
723
  )
730
724
 
731
725
  # Store registered services
732
726
  new_server_status.registered_services = services_metadata
733
- await asyncio.to_thread(
734
- new_server_status.save,
727
+ await new_server_status.asave(
735
728
  update_fields=["registered_services"]
736
729
  )
737
730
 
738
- # Mark as running
739
- await asyncio.to_thread(new_server_status.mark_running)
731
+ # Mark as running (Django 5.2: Native async ORM)
732
+ await new_server_status.amark_running()
740
733
 
741
734
  # Update reference
742
735
  self.server_status = new_server_status
@@ -745,8 +738,8 @@ class Command(BaseCommand):
745
738
  f"✅ Successfully re-registered server (ID: {new_server_status.id})"
746
739
  )
747
740
  else:
748
- # Record exists - just update heartbeat
749
- await asyncio.to_thread(self.server_status.mark_running)
741
+ # Record exists - just update heartbeat (Django 5.2: Native async ORM)
742
+ await self.server_status.amark_running()
750
743
  self.logger.debug(f"Heartbeat updated (interval: {interval}s)")
751
744
 
752
745
  except Exception as e:
@@ -785,17 +778,11 @@ class Command(BaseCommand):
785
778
 
786
779
  self.stdout.write("\n🛑 Shutting down gracefully...")
787
780
 
788
- # Mark server as stopping
781
+ # Mark server as stopping (sync context - signal handlers are sync)
789
782
  if server_status:
790
783
  try:
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
784
+ # ✅ Use Django 5.2+ async ORM instead of sync_to_async
785
+ asyncio.create_task(server_status.amark_stopping())
799
786
  except Exception as e:
800
787
  self.logger.warning(f"Could not mark server as stopping: {e}")
801
788
 
@@ -807,12 +794,11 @@ class Command(BaseCommand):
807
794
  except Exception as e:
808
795
  self.logger.error(f"Error stopping server: {e}")
809
796
 
810
- # Mark server as stopped (async-safe)
797
+ # Mark server as stopped (async-safe, Django 5.2: Native async ORM)
811
798
  if server_status:
812
799
  try:
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)())
800
+ # Use native async method
801
+ asyncio.create_task(server_status.amark_stopped())
816
802
  except Exception as e:
817
803
  self.logger.warning(f"Could not mark server as stopped: {e}")
818
804
 
@@ -207,6 +207,48 @@ class GRPCRequestLogManager(models.Manager):
207
207
  ]
208
208
  )
209
209
 
210
+ async def amark_success(
211
+ self,
212
+ log_instance,
213
+ duration_ms: int | None = None,
214
+ response_size: int | None = None,
215
+ response_data: dict | None = None,
216
+ ):
217
+ """
218
+ Mark request as successful (ASYNC - Django 5.2).
219
+
220
+ Args:
221
+ log_instance: GRPCRequestLog instance
222
+ duration_ms: Duration in milliseconds
223
+ response_size: Response size in bytes
224
+ response_data: Response data
225
+ """
226
+ from ..models import GRPCRequestLog
227
+
228
+ log_instance.status = GRPCRequestLog.StatusChoices.SUCCESS
229
+ log_instance.grpc_status_code = "OK"
230
+ log_instance.completed_at = timezone.now()
231
+
232
+ if duration_ms is not None:
233
+ log_instance.duration_ms = duration_ms
234
+
235
+ if response_size is not None:
236
+ log_instance.response_size = response_size
237
+
238
+ if response_data is not None:
239
+ log_instance.response_data = response_data
240
+
241
+ await log_instance.asave(
242
+ update_fields=[
243
+ "status",
244
+ "grpc_status_code",
245
+ "completed_at",
246
+ "duration_ms",
247
+ "response_size",
248
+ "response_data",
249
+ ]
250
+ )
251
+
210
252
  def mark_error(
211
253
  self,
212
254
  log_instance,
@@ -249,6 +291,48 @@ class GRPCRequestLogManager(models.Manager):
249
291
  ]
250
292
  )
251
293
 
294
+ async def amark_error(
295
+ self,
296
+ log_instance,
297
+ grpc_status_code: str,
298
+ error_message: str,
299
+ error_details: dict | None = None,
300
+ duration_ms: int | None = None,
301
+ ):
302
+ """
303
+ Mark request as failed (ASYNC - Django 5.2).
304
+
305
+ Args:
306
+ log_instance: GRPCRequestLog instance
307
+ grpc_status_code: gRPC status code
308
+ error_message: Error message
309
+ error_details: Additional error details
310
+ duration_ms: Duration in milliseconds
311
+ """
312
+ from ..models import GRPCRequestLog
313
+
314
+ log_instance.status = GRPCRequestLog.StatusChoices.ERROR
315
+ log_instance.grpc_status_code = grpc_status_code
316
+ log_instance.error_message = error_message
317
+ log_instance.completed_at = timezone.now()
318
+
319
+ if error_details is not None:
320
+ log_instance.error_details = error_details
321
+
322
+ if duration_ms is not None:
323
+ log_instance.duration_ms = duration_ms
324
+
325
+ await log_instance.asave(
326
+ update_fields=[
327
+ "status",
328
+ "grpc_status_code",
329
+ "error_message",
330
+ "error_details",
331
+ "completed_at",
332
+ "duration_ms",
333
+ ]
334
+ )
335
+
252
336
  def mark_cancelled(
253
337
  self,
254
338
  log_instance,
@@ -32,7 +32,7 @@ class GRPCServerStatusManager(models.Manager):
32
32
  enable_health_check: bool = True,
33
33
  ):
34
34
  """
35
- Start tracking a new server instance.
35
+ Start tracking a new server instance (SYNC).
36
36
 
37
37
  Args:
38
38
  host: Server host address
@@ -88,9 +88,75 @@ class GRPCServerStatusManager(models.Manager):
88
88
 
89
89
  return status
90
90
 
91
+ async def astart_server(
92
+ self,
93
+ host: str,
94
+ port: int,
95
+ pid: int = None,
96
+ max_workers: int = 10,
97
+ enable_reflection: bool = False,
98
+ enable_health_check: bool = True,
99
+ ):
100
+ """
101
+ Start tracking a new server instance (ASYNC - Django 5.2).
102
+
103
+ Args:
104
+ host: Server host address
105
+ port: Server port
106
+ pid: Process ID (defaults to current process)
107
+ max_workers: Maximum worker threads
108
+ enable_reflection: Whether reflection is enabled
109
+ enable_health_check: Whether health check is enabled
110
+
111
+ Returns:
112
+ GRPCServerStatus instance
113
+
114
+ Example:
115
+ >>> status = await GRPCServerStatus.objects.astart_server(
116
+ ... host="[::]",
117
+ ... port=50051,
118
+ ... pid=12345
119
+ ... )
120
+ >>> status.is_running
121
+ True
122
+
123
+ Note:
124
+ External/internal server detection is automatic based on env_mode.
125
+ Production mode assumes external server (Docker), dev/test assumes local.
126
+ """
127
+ if pid is None:
128
+ pid = os.getpid()
129
+
130
+ hostname = socket.gethostname()
131
+ address = f"{host}:{port}"
132
+ instance_id = f"{hostname}:{port}:{pid}"
133
+
134
+ # Mark any existing server at this address as stopped (async)
135
+ await self.astop_servers_at_address(address)
136
+
137
+ # Create or update server status (handles restart with same instance_id)
138
+ status, created = await self.aupdate_or_create(
139
+ instance_id=instance_id,
140
+ defaults={
141
+ "host": host,
142
+ "port": port,
143
+ "address": address,
144
+ "pid": pid,
145
+ "hostname": hostname,
146
+ "status": self.model.StatusChoices.STARTING,
147
+ "max_workers": max_workers,
148
+ "enable_reflection": enable_reflection,
149
+ "enable_health_check": enable_health_check,
150
+ "started_at": timezone.now(),
151
+ "last_heartbeat": timezone.now(),
152
+ },
153
+ )
154
+
155
+ return status
156
+
91
157
  def get_current_server(self, address: str = None) -> Optional["GRPCServerStatus"]:
92
158
  """
93
- Get the currently running server.
159
+ Get the currently running server (SYNC).
94
160
 
95
161
  Args:
96
162
  address: Optional address filter (host:port)
@@ -122,6 +188,40 @@ class GRPCServerStatusManager(models.Manager):
122
188
 
123
189
  return None
124
190
 
191
+ async def aget_current_server(self, address: str = None) -> Optional["GRPCServerStatus"]:
192
+ """
193
+ Get the currently running server (ASYNC - Django 5.2).
194
+
195
+ Args:
196
+ address: Optional address filter (host:port)
197
+
198
+ Returns:
199
+ GRPCServerStatus instance or None
200
+
201
+ Example:
202
+ >>> server = await GRPCServerStatus.objects.aget_current_server()
203
+ >>> if server and server.is_running:
204
+ ... print(f"Server running for {server.uptime_display}")
205
+ """
206
+ queryset = self.filter(
207
+ status__in=[
208
+ self.model.StatusChoices.STARTING,
209
+ self.model.StatusChoices.RUNNING,
210
+ ]
211
+ )
212
+
213
+ if address:
214
+ queryset = queryset.filter(address=address)
215
+
216
+ # Get most recent (Django 5.2: afirst)
217
+ server = await queryset.order_by("-started_at").afirst()
218
+
219
+ # Verify it's actually running
220
+ if server and server.is_running:
221
+ return server
222
+
223
+ return None
224
+
125
225
  def get_running_servers(self):
126
226
  """
127
227
  Get all currently running servers.
@@ -183,7 +283,7 @@ class GRPCServerStatusManager(models.Manager):
183
283
 
184
284
  def stop_servers_at_address(self, address: str):
185
285
  """
186
- Stop all servers at a specific address.
286
+ Stop all servers at a specific address (SYNC).
187
287
 
188
288
  Args:
189
289
  address: Server address (host:port)
@@ -202,6 +302,29 @@ class GRPCServerStatusManager(models.Manager):
202
302
  for server in servers:
203
303
  server.mark_stopped("Replaced by new server instance")
204
304
 
305
+ async def astop_servers_at_address(self, address: str):
306
+ """
307
+ Stop all servers at a specific address (ASYNC - Django 5.2).
308
+
309
+ Args:
310
+ address: Server address (host:port)
311
+
312
+ Example:
313
+ >>> await GRPCServerStatus.objects.astop_servers_at_address("[::]:50051")
314
+ """
315
+ servers = []
316
+ async for server in self.filter(
317
+ address=address,
318
+ status__in=[
319
+ self.model.StatusChoices.STARTING,
320
+ self.model.StatusChoices.RUNNING,
321
+ ],
322
+ ):
323
+ servers.append(server)
324
+
325
+ for server in servers:
326
+ await server.amark_stopped("Replaced by new server instance")
327
+
205
328
  def cleanup_stale_servers(self, hours: int = 24):
206
329
  """
207
330
  Mark stale servers as stopped.
@@ -184,11 +184,17 @@ class GrpcApiKey(models.Model):
184
184
  return f"{self.key[:4]}...{self.key[-4:]}"
185
185
 
186
186
  def mark_used(self) -> None:
187
- """Mark this key as used (update last_used_at and increment counter)."""
187
+ """Mark this key as used (update last_used_at and increment counter) (SYNC)."""
188
188
  self.last_used_at = timezone.now()
189
189
  self.request_count += 1
190
190
  self.save(update_fields=["last_used_at", "request_count"])
191
191
 
192
+ async def amark_used(self) -> None:
193
+ """Mark this key as used (update last_used_at and increment counter) (ASYNC - Django 5.2)."""
194
+ self.last_used_at = timezone.now()
195
+ self.request_count += 1
196
+ await self.asave(update_fields=["last_used_at", "request_count"])
197
+
192
198
  def revoke(self) -> None:
193
199
  """Revoke this key (set is_active=False)."""
194
200
  self.is_active = False
@@ -271,24 +271,43 @@ class GRPCServerStatus(models.Model):
271
271
  return False
272
272
 
273
273
  def mark_running(self):
274
- """Mark server as running."""
274
+ """Mark server as running (SYNC)."""
275
275
  self.status = self.StatusChoices.RUNNING
276
276
  self.error_message = None
277
277
  self.save(update_fields=["status", "error_message", "updated_at", "last_heartbeat"])
278
278
 
279
+ async def amark_running(self):
280
+ """Mark server as running (ASYNC - Django 5.2)."""
281
+ self.status = self.StatusChoices.RUNNING
282
+ self.error_message = None
283
+ await self.asave(update_fields=["status", "error_message", "updated_at", "last_heartbeat"])
284
+
279
285
  def mark_stopping(self):
280
- """Mark server as stopping."""
286
+ """Mark server as stopping (SYNC)."""
281
287
  self.status = self.StatusChoices.STOPPING
282
288
  self.save(update_fields=["status", "updated_at", "last_heartbeat"])
283
289
 
290
+ async def amark_stopping(self):
291
+ """Mark server as stopping (ASYNC - Django 5.2)."""
292
+ self.status = self.StatusChoices.STOPPING
293
+ await self.asave(update_fields=["status", "updated_at", "last_heartbeat"])
294
+
284
295
  def mark_stopped(self, error_message: str = None):
285
- """Mark server as stopped."""
296
+ """Mark server as stopped (SYNC)."""
286
297
  self.status = self.StatusChoices.STOPPED
287
298
  self.stopped_at = timezone.now()
288
299
  if error_message:
289
300
  self.error_message = error_message
290
301
  self.save(update_fields=["status", "stopped_at", "error_message", "updated_at"])
291
302
 
303
+ async def amark_stopped(self, error_message: str = None):
304
+ """Mark server as stopped (ASYNC - Django 5.2)."""
305
+ self.status = self.StatusChoices.STOPPED
306
+ self.stopped_at = timezone.now()
307
+ if error_message:
308
+ self.error_message = error_message
309
+ await self.asave(update_fields=["status", "stopped_at", "error_message", "updated_at"])
310
+
292
311
  def mark_error(self, error_message: str):
293
312
  """Mark server as error."""
294
313
  self.status = self.StatusChoices.ERROR
@@ -1,36 +1,117 @@
1
1
  """
2
2
  gRPC services utilities.
3
3
 
4
- Provides service discovery, base classes, config helpers, service registry,
5
- monitoring, and testing services for gRPC.
6
- """
4
+ Provides organized gRPC service components:
5
+ - **streaming**: Universal bidirectional streaming (NEW)
6
+ - **routing**: Cross-process command routing (NEW)
7
+ - **client**: gRPC client utilities
8
+ - **discovery**: Service discovery and registry
9
+ - **management**: Proto files and config management
10
+ - **monitoring**: Service monitoring and testing
11
+ - **rendering**: Content generation (charts, etc.)
12
+ - **base**: Base service classes
13
+
14
+ **Quick Imports**:
15
+ ```python
16
+ # New universal components
17
+ from django_cfg.apps.integrations.grpc.services.streaming import (
18
+ BidirectionalStreamingService,
19
+ ConfigPresets,
20
+ )
7
21
 
8
- from .base import AuthRequiredService, BaseService, ReadOnlyService
9
- from .config_helper import (
10
- get_enabled_apps,
11
- get_grpc_auth_config,
12
- get_grpc_config,
13
- get_grpc_config_or_default,
14
- get_grpc_server_config,
15
- is_grpc_enabled,
22
+ from django_cfg.apps.integrations.grpc.services.routing import (
23
+ CrossProcessCommandRouter,
24
+ CrossProcessConfig,
16
25
  )
17
- from .discovery import ServiceDiscovery, discover_and_register_services
18
- from .grpc_client import DynamicGRPCClient
19
- from .monitoring_service import MonitoringService
20
- from .proto_files_manager import ProtoFilesManager
21
- from .service_registry import ServiceRegistryManager
22
- from .testing_service import TestingService
26
+
27
+ # Existing services
28
+ from django_cfg.apps.integrations.grpc.services import (
29
+ BaseService,
30
+ ServiceDiscovery,
31
+ )
32
+ ```
33
+
34
+ Created: 2025-11-07
35
+ Status: %%PRODUCTION%%
36
+ """
37
+
38
+ # Lazy imports to avoid Django initialization on import
39
+ # Import only when actually accessed via __getattr__
40
+
41
+ def __getattr__(name):
42
+ """Lazy import to avoid Django setup on package import."""
43
+
44
+ # Base service classes
45
+ if name in ('AuthRequiredService', 'BaseService', 'ReadOnlyService'):
46
+ from .base import AuthRequiredService, BaseService, ReadOnlyService
47
+ return locals()[name]
48
+
49
+ # Management utilities
50
+ if name in ('get_enabled_apps', 'get_grpc_auth_config', 'get_grpc_config',
51
+ 'get_grpc_config_or_default', 'get_grpc_server_config', 'is_grpc_enabled'):
52
+ from .management.config_helper import (
53
+ get_enabled_apps,
54
+ get_grpc_auth_config,
55
+ get_grpc_config,
56
+ get_grpc_config_or_default,
57
+ get_grpc_server_config,
58
+ is_grpc_enabled,
59
+ )
60
+ return locals()[name]
61
+
62
+ if name == 'ProtoFilesManager':
63
+ from .management.proto_manager import ProtoFilesManager
64
+ return ProtoFilesManager
65
+
66
+ # Discovery
67
+ if name == 'ServiceDiscovery':
68
+ from .discovery.discovery import ServiceDiscovery
69
+ return ServiceDiscovery
70
+
71
+ if name == 'discover_and_register_services':
72
+ from .discovery.discovery import discover_and_register_services
73
+ return discover_and_register_services
74
+
75
+ if name == 'ServiceRegistryManager':
76
+ from .discovery.registry import ServiceRegistryManager
77
+ return ServiceRegistryManager
78
+
79
+ # Client
80
+ if name == 'DynamicGRPCClient':
81
+ from .client.client import DynamicGRPCClient
82
+ return DynamicGRPCClient
83
+
84
+ # Monitoring
85
+ if name == 'MonitoringService':
86
+ from .monitoring.monitoring import MonitoringService
87
+ return MonitoringService
88
+
89
+ if name == 'TestingService':
90
+ from .monitoring.testing import TestingService
91
+ return TestingService
92
+
93
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
94
+
23
95
 
24
96
  __all__ = [
97
+ # Base classes
25
98
  "BaseService",
26
99
  "ReadOnlyService",
27
100
  "AuthRequiredService",
101
+
102
+ # Discovery
28
103
  "ServiceDiscovery",
29
104
  "discover_and_register_services",
30
105
  "ServiceRegistryManager",
106
+
107
+ # Monitoring
31
108
  "MonitoringService",
32
109
  "TestingService",
110
+
111
+ # Client
33
112
  "DynamicGRPCClient",
113
+
114
+ # Management
34
115
  "ProtoFilesManager",
35
116
  "get_grpc_config",
36
117
  "get_grpc_config_or_default",
@@ -38,4 +119,8 @@ __all__ = [
38
119
  "get_grpc_server_config",
39
120
  "get_grpc_auth_config",
40
121
  "get_enabled_apps",
122
+
123
+ # New components available via subpackages:
124
+ # - streaming: BidirectionalStreamingService, ConfigPresets, etc.
125
+ # - routing: CrossProcessCommandRouter, CrossProcessConfig
41
126
  ]