django-cfg 1.5.20__py3-none-any.whl → 1.5.29__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 (88) 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 +47 -0
  5. django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
  6. django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -37
  7. django_cfg/apps/integrations/centrifugo/views/wrapper.py +25 -23
  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 +21 -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} +215 -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/groups/manager.py +25 -18
  66. django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
  67. django_cfg/modules/django_logging/django_logger.py +58 -19
  68. django_cfg/pyproject.toml +3 -3
  69. django_cfg/static/frontend/admin.zip +0 -0
  70. django_cfg/templates/admin/index.html +0 -39
  71. django_cfg/utils/pool_monitor.py +320 -0
  72. django_cfg/utils/smart_defaults.py +233 -7
  73. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
  74. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/RECORD +87 -59
  75. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
  76. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
  77. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
  78. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
  79. /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
  80. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
  81. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
  82. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
  83. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
  84. /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
  85. /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
  86. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
  87. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
  88. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.5.20"
35
+ __version__ = "1.5.29"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Setup warnings debug early (checks env var only at this point)
@@ -23,6 +23,7 @@ Example:
23
23
  """
24
24
 
25
25
  from .services.client.client import CentrifugoClient, get_centrifugo_client, PublishResponse
26
+ from .services.client.direct_client import DirectCentrifugoClient
26
27
  from .services.client.config import DjangoCfgCentrifugoConfig
27
28
  from .services.client.exceptions import (
28
29
  CentrifugoBaseException,
@@ -42,6 +43,7 @@ __all__ = [
42
43
  "DjangoCfgCentrifugoConfig",
43
44
  # Client
44
45
  "CentrifugoClient",
46
+ "DirectCentrifugoClient",
45
47
  "get_centrifugo_client",
46
48
  "PublishResponse",
47
49
  # Logging
@@ -396,7 +396,7 @@ class CentrifugoClient:
396
396
 
397
397
  error_code = type(last_error).__name__ if last_error else "unknown"
398
398
  error_message = str(last_error) if last_error else "Unknown error"
399
- CentrifugoLogger.mark_failed(
399
+ await CentrifugoLogger.mark_failed_async(
400
400
  log_entry,
401
401
  error_code=error_code,
402
402
  error_message=error_message,
@@ -400,6 +400,53 @@ class CentrifugoLogger:
400
400
  extra={"message_id": getattr(log_entry, "message_id", "unknown")},
401
401
  )
402
402
 
403
+ @staticmethod
404
+ async def mark_failed_async(
405
+ log_entry: Any,
406
+ error_code: str,
407
+ error_message: str,
408
+ duration_ms: int | None = None,
409
+ ) -> None:
410
+ """
411
+ Mark publish operation as failed (async version).
412
+
413
+ Args:
414
+ log_entry: CentrifugoLog instance
415
+ error_code: Error code
416
+ error_message: Error message
417
+ duration_ms: Duration in milliseconds
418
+ """
419
+ if log_entry is None:
420
+ return
421
+
422
+ try:
423
+ from asgiref.sync import sync_to_async
424
+ from ..models import CentrifugoLog
425
+
426
+ await sync_to_async(CentrifugoLog.objects.mark_failed)(
427
+ log_instance=log_entry,
428
+ error_code=error_code,
429
+ error_message=error_message,
430
+ duration_ms=duration_ms,
431
+ )
432
+
433
+ logger.error(
434
+ f"Centrifugo publish failed: {log_entry.message_id}",
435
+ extra={
436
+ "message_id": log_entry.message_id,
437
+ "channel": log_entry.channel,
438
+ "error_code": error_code,
439
+ "error_message": error_message,
440
+ "duration_ms": duration_ms,
441
+ },
442
+ )
443
+
444
+ except Exception as e:
445
+ logger.error(
446
+ f"Failed to mark Centrifugo log as failed: {e}",
447
+ extra={"message_id": getattr(log_entry, "message_id", "unknown")},
448
+ )
449
+
403
450
  @staticmethod
404
451
  async def mark_timeout_async(
405
452
  log_entry: Any,
@@ -5,7 +5,6 @@ Proxies requests to Centrifugo server API with authentication and type safety.
5
5
  Provides Django endpoints that map to Centrifugo HTTP API methods.
6
6
  """
7
7
 
8
- import asyncio
9
8
  import httpx
10
9
  from django.http import JsonResponse
11
10
  from django_cfg.modules.django_logging import get_logger
@@ -113,10 +112,10 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
113
112
  },
114
113
  )
115
114
  @action(detail=False, methods=["post"], url_path="info")
116
- def info(self, request):
117
- """Get Centrifugo server information."""
115
+ async def info(self, request):
116
+ """Get Centrifugo server information (ASYNC)."""
118
117
  try:
119
- result = asyncio.run(self._call_centrifugo_api("info", params={}))
118
+ result = await self._call_centrifugo_api("info", params={})
120
119
 
121
120
  # Check for Centrifugo API error
122
121
  if "error" in result and result["error"]:
@@ -147,13 +146,13 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
147
146
  },
148
147
  )
149
148
  @action(detail=False, methods=["post"], url_path="channels")
150
- def channels(self, request):
151
- """List active channels."""
149
+ async def channels(self, request):
150
+ """List active channels (ASYNC)."""
152
151
  try:
153
152
  req_data = CentrifugoChannelsRequest(**request.data)
154
- result = asyncio.run(self._call_centrifugo_api(
153
+ result = await self._call_centrifugo_api(
155
154
  "channels", params=req_data.model_dump(exclude_none=True)
156
- ))
155
+ )
157
156
 
158
157
  # Check for Centrifugo API error
159
158
  if "error" in result and result["error"]:
@@ -183,13 +182,13 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
183
182
  },
184
183
  )
185
184
  @action(detail=False, methods=["post"], url_path="presence")
186
- def presence(self, request):
187
- """Get channel presence (active subscribers)."""
185
+ async def presence(self, request):
186
+ """Get channel presence (active subscribers) (ASYNC)."""
188
187
  try:
189
188
  req_data = CentrifugoPresenceRequest(**request.data)
190
- result = asyncio.run(self._call_centrifugo_api(
189
+ result = await self._call_centrifugo_api(
191
190
  "presence", params=req_data.model_dump()
192
- ))
191
+ )
193
192
 
194
193
  # Check for Centrifugo API error (e.g., code 108 "not available")
195
194
  if "error" in result and result["error"]:
@@ -219,13 +218,13 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
219
218
  },
220
219
  )
221
220
  @action(detail=False, methods=["post"], url_path="presence-stats")
222
- def presence_stats(self, request):
223
- """Get channel presence statistics."""
221
+ async def presence_stats(self, request):
222
+ """Get channel presence statistics (ASYNC)."""
224
223
  try:
225
224
  req_data = CentrifugoPresenceStatsRequest(**request.data)
226
- result = asyncio.run(self._call_centrifugo_api(
225
+ result = await self._call_centrifugo_api(
227
226
  "presence_stats", params=req_data.model_dump()
228
- ))
227
+ )
229
228
 
230
229
  # Check for Centrifugo API error (e.g., code 108 "not available")
231
230
  if "error" in result and result["error"]:
@@ -255,13 +254,13 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
255
254
  },
256
255
  )
257
256
  @action(detail=False, methods=["post"], url_path="history")
258
- def history(self, request):
259
- """Get channel message history."""
257
+ async def history(self, request):
258
+ """Get channel message history (ASYNC)."""
260
259
  try:
261
260
  req_data = CentrifugoHistoryRequest(**request.data)
262
- result = asyncio.run(self._call_centrifugo_api(
261
+ result = await self._call_centrifugo_api(
263
262
  "history", params=req_data.model_dump(exclude_none=True)
264
- ))
263
+ )
265
264
 
266
265
  # Check for Centrifugo API error (e.g., code 108 "not available")
267
266
  if "error" in result and result["error"]:
@@ -359,19 +358,17 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
359
358
  status=status.HTTP_500_INTERNAL_SERVER_ERROR
360
359
  )
361
360
 
362
- def __del__(self):
363
- """Cleanup HTTP client on deletion."""
361
+ async def cleanup(self):
362
+ """
363
+ Explicit async cleanup method for HTTP client.
364
+
365
+ Note: Django handles ViewSet lifecycle automatically.
366
+ This method is provided for explicit cleanup if needed,
367
+ but httpx.AsyncClient will be garbage collected normally.
368
+ """
364
369
  if self._http_client:
365
- import asyncio
366
-
367
- try:
368
- loop = asyncio.get_event_loop()
369
- if loop.is_running():
370
- loop.create_task(self._http_client.aclose())
371
- else:
372
- loop.run_until_complete(self._http_client.aclose())
373
- except Exception:
374
- pass # Ignore cleanup errors
370
+ await self._http_client.aclose()
371
+ self._http_client = None
375
372
 
376
373
 
377
374
  __all__ = ["CentrifugoAdminAPIViewSet"]
@@ -5,7 +5,6 @@ Provides endpoints for live testing of Centrifugo integration from dashboard.
5
5
  Includes connection tokens, publish proxying, and ACK management.
6
6
  """
7
7
 
8
- import asyncio
9
8
  import time
10
9
  from datetime import datetime, timedelta
11
10
  from typing import Any, Dict
@@ -127,23 +126,21 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
127
126
  },
128
127
  )
129
128
  @action(detail=False, methods=["post"], url_path="publish-test")
130
- def publish_test(self, request):
129
+ async def publish_test(self, request):
131
130
  """
132
- Publish test message via wrapper.
131
+ Publish test message via wrapper (ASYNC).
133
132
 
134
133
  Proxies request to Centrifugo wrapper with ACK tracking support.
135
134
  """
136
135
  try:
137
136
  req_data = PublishTestRequest(**request.data)
138
137
 
139
- # Call wrapper API
140
- result = asyncio.run(
141
- self._publish_to_wrapper(
142
- channel=req_data.channel,
143
- data=req_data.data,
144
- wait_for_ack=req_data.wait_for_ack,
145
- ack_timeout=req_data.ack_timeout,
146
- )
138
+ # Call wrapper API (ASYNC - no asyncio.run()!)
139
+ result = await self._publish_to_wrapper(
140
+ channel=req_data.channel,
141
+ data=req_data.data,
142
+ wait_for_ack=req_data.wait_for_ack,
143
+ ack_timeout=req_data.ack_timeout,
147
144
  )
148
145
 
149
146
  response = PublishTestResponse(
@@ -180,20 +177,18 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
180
177
  },
181
178
  )
182
179
  @action(detail=False, methods=["post"], url_path="send-ack")
183
- def send_ack(self, request):
180
+ async def send_ack(self, request):
184
181
  """
185
- Send manual ACK for message.
182
+ Send manual ACK for message (ASYNC).
186
183
 
187
184
  Proxies ACK to wrapper for testing ACK flow.
188
185
  """
189
186
  try:
190
187
  req_data = ManualAckRequest(**request.data)
191
188
 
192
- # Send ACK to wrapper
193
- result = asyncio.run(
194
- self._send_ack_to_wrapper(
195
- message_id=req_data.message_id, client_id=req_data.client_id
196
- )
189
+ # Send ACK to wrapper (ASYNC - no asyncio.run()!)
190
+ result = await self._send_ack_to_wrapper(
191
+ message_id=req_data.message_id, client_id=req_data.client_id
197
192
  )
198
193
 
199
194
  response = ManualAckResponse(
@@ -334,9 +329,9 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
334
329
  },
335
330
  )
336
331
  @action(detail=False, methods=["post"], url_path="publish-with-logging")
337
- def publish_with_logging(self, request):
332
+ async def publish_with_logging(self, request):
338
333
  """
339
- Publish message using CentrifugoClient with database logging.
334
+ Publish message using CentrifugoClient with database logging (ASYNC).
340
335
 
341
336
  This endpoint uses the production CentrifugoClient which logs all
342
337
  publishes to the database (CentrifugoLog model).
@@ -347,25 +342,24 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
347
342
  # Use CentrifugoClient for publishing
348
343
  client = CentrifugoClient()
349
344
 
350
- # Publish message
351
- result = asyncio.run(
352
- client.publish_with_ack(
345
+ # Publish message (ASYNC - no asyncio.run()!)
346
+ if req_data.wait_for_ack:
347
+ result = await client.publish_with_ack(
353
348
  channel=req_data.channel,
354
349
  data=req_data.data,
355
- ack_timeout=req_data.ack_timeout if req_data.wait_for_ack else None,
350
+ ack_timeout=req_data.ack_timeout,
356
351
  user=request.user if request.user.is_authenticated else None,
357
352
  caller_ip=request.META.get("REMOTE_ADDR"),
358
353
  user_agent=request.META.get("HTTP_USER_AGENT"),
359
354
  )
360
- if req_data.wait_for_ack
361
- else client.publish(
355
+ else:
356
+ result = await client.publish(
362
357
  channel=req_data.channel,
363
358
  data=req_data.data,
364
359
  user=request.user if request.user.is_authenticated else None,
365
360
  caller_ip=request.META.get("REMOTE_ADDR"),
366
361
  user_agent=request.META.get("HTTP_USER_AGENT"),
367
362
  )
368
- )
369
363
 
370
364
  # Convert PublishResponse to dict
371
365
  response_data = {
@@ -386,17 +380,17 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
386
380
  status=status.HTTP_500_INTERNAL_SERVER_ERROR,
387
381
  )
388
382
 
389
- def __del__(self):
390
- """Cleanup HTTP client on deletion."""
383
+ async def cleanup(self):
384
+ """
385
+ Explicit async cleanup method for HTTP client.
386
+
387
+ Note: Django handles ViewSet lifecycle automatically.
388
+ This method is provided for explicit cleanup if needed,
389
+ but httpx.AsyncClient will be garbage collected normally.
390
+ """
391
391
  if self._http_client:
392
- try:
393
- loop = asyncio.get_event_loop()
394
- if loop.is_running():
395
- loop.create_task(self._http_client.aclose())
396
- else:
397
- loop.run_until_complete(self._http_client.aclose())
398
- except Exception:
399
- pass
392
+ await self._http_client.aclose()
393
+ self._http_client = None
400
394
 
401
395
 
402
396
  __all__ = ["CentrifugoTestingAPIViewSet"]
@@ -5,12 +5,12 @@ Provides /api/publish endpoint that acts as a proxy to Centrifugo
5
5
  with ACK tracking and database logging.
6
6
  """
7
7
 
8
- import asyncio
9
8
  import time
10
9
  import uuid
11
10
  from typing import Any, Dict
12
11
 
13
12
  import httpx
13
+ from django.db import transaction
14
14
  from django.http import JsonResponse
15
15
  from django.utils.decorators import method_decorator
16
16
  from django.views import View
@@ -55,15 +55,19 @@ class PublishResponse(BaseModel):
55
55
 
56
56
 
57
57
  @method_decorator(csrf_exempt, name='dispatch')
58
+ @method_decorator(transaction.non_atomic_requests, name='dispatch')
58
59
  class PublishWrapperView(View):
59
60
  """
60
- Centrifugo publish wrapper endpoint.
61
+ Centrifugo publish wrapper endpoint (ASYNC).
61
62
 
62
63
  Provides /api/publish endpoint that:
63
64
  - Accepts publish requests from CentrifugoClient
64
65
  - Logs to database (CentrifugoLog)
65
66
  - Proxies to Centrifugo HTTP API
66
67
  - Returns publish result with ACK tracking
68
+
69
+ NOTE: This is an async view for proper async/await handling.
70
+ Using asyncio.run() in sync views causes event loop conflicts.
67
71
  """
68
72
 
69
73
  def __init__(self, *args, **kwargs):
@@ -181,9 +185,9 @@ class PublishWrapperView(View):
181
185
 
182
186
  raise
183
187
 
184
- def post(self, request):
188
+ async def post(self, request):
185
189
  """
186
- Handle POST /api/publish request.
190
+ Handle POST /api/publish request (ASYNC).
187
191
 
188
192
  Request body:
189
193
  {
@@ -213,15 +217,13 @@ class PublishWrapperView(View):
213
217
  # Generate message ID if not provided
214
218
  message_id = req_data.message_id or str(uuid.uuid4())
215
219
 
216
- # Publish to Centrifugo
217
- result = asyncio.run(
218
- self._publish_to_centrifugo(
219
- channel=req_data.channel,
220
- data=req_data.data,
221
- wait_for_ack=req_data.wait_for_ack,
222
- ack_timeout=req_data.ack_timeout,
223
- message_id=message_id,
224
- )
220
+ # Publish to Centrifugo (ASYNC - no asyncio.run()!)
221
+ result = await self._publish_to_centrifugo(
222
+ channel=req_data.channel,
223
+ data=req_data.data,
224
+ wait_for_ack=req_data.wait_for_ack,
225
+ ack_timeout=req_data.ack_timeout,
226
+ message_id=message_id,
225
227
  )
226
228
 
227
229
  response = PublishResponse(**result)
@@ -241,17 +243,17 @@ class PublishWrapperView(View):
241
243
  status=500,
242
244
  )
243
245
 
244
- def __del__(self):
245
- """Cleanup HTTP client on deletion."""
246
+ async def cleanup(self):
247
+ """
248
+ Explicit async cleanup method for HTTP client.
249
+
250
+ Note: Django handles View lifecycle automatically.
251
+ This method is provided for explicit cleanup if needed,
252
+ but httpx.AsyncClient will be garbage collected normally.
253
+ """
246
254
  if self._http_client:
247
- try:
248
- loop = asyncio.get_event_loop()
249
- if loop.is_running():
250
- loop.create_task(self._http_client.aclose())
251
- else:
252
- loop.run_until_complete(self._http_client.aclose())
253
- except Exception:
254
- pass
255
+ await self._http_client.aclose()
256
+ self._http_client = None
255
257
 
256
258
 
257
259
  __all__ = ["PublishWrapperView"]
@@ -170,10 +170,11 @@ class ApiKeyAuthInterceptor(grpc.aio.ServerInterceptor):
170
170
  logger.debug("API key matches Django SECRET_KEY")
171
171
  # For SECRET_KEY, return first superuser or None (no api_key instance)
172
172
  try:
173
- # Wrap Django ORM in asyncio.to_thread()
174
- superuser = await asyncio.to_thread(
175
- lambda: User.objects.filter(is_superuser=True, is_active=True).first()
176
- )
173
+ # Django 5.2: Native async ORM
174
+ superuser = await User.objects.filter(
175
+ is_superuser=True, is_active=True
176
+ ).afirst()
177
+
177
178
  if superuser:
178
179
  return superuser, None
179
180
  else:
@@ -187,14 +188,14 @@ class ApiKeyAuthInterceptor(grpc.aio.ServerInterceptor):
187
188
  try:
188
189
  from django_cfg.apps.integrations.grpc.models import GrpcApiKey
189
190
 
190
- # Wrap Django ORM in asyncio.to_thread()
191
- api_key_obj = await asyncio.to_thread(
192
- lambda: GrpcApiKey.objects.filter(key=api_key, is_active=True).first()
193
- )
191
+ # Django 5.2: Native async ORM
192
+ api_key_obj = await GrpcApiKey.objects.filter(
193
+ key=api_key, is_active=True
194
+ ).afirst()
194
195
 
195
196
  if api_key_obj and api_key_obj.is_valid:
196
- # Update usage tracking (also wrapped in to_thread)
197
- await asyncio.to_thread(api_key_obj.mark_used)
197
+ # Update usage tracking (async method call)
198
+ await api_key_obj.amark_used()
198
199
  logger.debug(f"Valid API key for user {api_key_obj.user.id} ({api_key_obj.user.username})")
199
200
  return api_key_obj.user, api_key_obj
200
201
  else:
@@ -50,7 +50,7 @@ class Command(AdminCommand):
50
50
 
51
51
  def handle(self, *args, **options):
52
52
  from django_cfg.apps.integrations.grpc.utils.proto_gen import generate_proto_for_app
53
- from django_cfg.apps.integrations.grpc.services.config_helper import get_grpc_config
53
+ from django_cfg.apps.integrations.grpc.services.management.config_helper import get_grpc_config
54
54
 
55
55
  # Get gRPC config
56
56
  grpc_config = get_grpc_config()
@@ -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,10 @@ 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
+ server_status.mark_stopping()
799
785
  except Exception as e:
800
786
  self.logger.warning(f"Could not mark server as stopping: {e}")
801
787
 
@@ -807,12 +793,11 @@ class Command(BaseCommand):
807
793
  except Exception as e:
808
794
  self.logger.error(f"Error stopping server: {e}")
809
795
 
810
- # Mark server as stopped (async-safe)
796
+ # Mark server as stopped (async-safe, Django 5.2: Native async ORM)
811
797
  if server_status:
812
798
  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)())
799
+ # Use native async method
800
+ asyncio.create_task(server_status.amark_stopped())
816
801
  except Exception as e:
817
802
  self.logger.warning(f"Could not mark server as stopped: {e}")
818
803