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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/logging.py +90 -14
- django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +47 -43
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +41 -29
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +1 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +22 -36
- django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
- django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
- django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/demo.py +1 -1
- django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/test_publish.py +4 -4
- django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
- django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
- django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
- django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
- django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
- django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
- django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
- django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +62 -55
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +216 -5
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
- django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
- django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
- django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
- django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
- django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
- django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
- django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
- django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
- django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
- django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
- django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
- django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
- django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
- django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
- django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +55 -8
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -1
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -0
- django_cfg/management/commands/pool_status.py +153 -0
- django_cfg/middleware/pool_cleanup.py +261 -0
- django_cfg/models/api/grpc/config.py +2 -2
- django_cfg/models/infrastructure/database/config.py +16 -0
- django_cfg/models/infrastructure/database/converters.py +2 -0
- django_cfg/modules/django_admin/utils/html/composition.py +57 -13
- django_cfg/modules/django_admin/utils/html_builder.py +1 -0
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +12 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +22 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/validation-events.ts.jinja +133 -0
- django_cfg/modules/django_client/core/groups/manager.py +25 -18
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_client/urls.py +38 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/modules/django_twilio/email_otp.py +3 -1
- django_cfg/modules/django_twilio/sms.py +3 -1
- django_cfg/modules/django_twilio/unified.py +6 -2
- django_cfg/modules/django_twilio/whatsapp.py +3 -1
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +17 -57
- django_cfg/utils/pool_monitor.py +320 -0
- django_cfg/utils/smart_defaults.py +233 -7
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/METADATA +75 -5
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/RECORD +97 -68
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
- /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
|
@@ -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.
|
|
399
|
+
await CentrifugoLogger.mark_failed_async(
|
|
400
400
|
log_entry,
|
|
401
401
|
error_code=error_code,
|
|
402
402
|
error_message=error_message,
|
|
@@ -8,6 +8,7 @@ Mirrors RPCLogger patterns from legacy WebSocket solution for easy migration.
|
|
|
8
8
|
import time
|
|
9
9
|
from typing import Any, Optional
|
|
10
10
|
|
|
11
|
+
from django.utils import timezone
|
|
11
12
|
from django_cfg.modules.django_logging import get_logger
|
|
12
13
|
|
|
13
14
|
logger = get_logger("centrifugo")
|
|
@@ -87,11 +88,11 @@ class CentrifugoLogger:
|
|
|
87
88
|
|
|
88
89
|
logger.info(f"✅ Creating CentrifugoLog entry for {message_id} (async)")
|
|
89
90
|
try:
|
|
90
|
-
from asgiref.sync import sync_to_async
|
|
91
91
|
from ..models import CentrifugoLog
|
|
92
92
|
|
|
93
|
-
#
|
|
94
|
-
|
|
93
|
+
# ✅ Use Django 5.2+ async ORM instead of sync_to_async
|
|
94
|
+
# This prevents connection leaks from sync_to_async threads
|
|
95
|
+
log_entry = await CentrifugoLog.objects.acreate(
|
|
95
96
|
message_id=message_id,
|
|
96
97
|
channel=channel,
|
|
97
98
|
data=data,
|
|
@@ -234,14 +235,17 @@ class CentrifugoLogger:
|
|
|
234
235
|
return
|
|
235
236
|
|
|
236
237
|
try:
|
|
237
|
-
from asgiref.sync import sync_to_async
|
|
238
238
|
from ..models import CentrifugoLog
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
# ✅ Use Django 5.2+ async ORM instead of sync_to_async
|
|
241
|
+
log_entry.status = CentrifugoLog.StatusChoices.SUCCESS
|
|
242
|
+
log_entry.acks_received = acks_received
|
|
243
|
+
log_entry.completed_at = timezone.now()
|
|
244
|
+
|
|
245
|
+
if duration_ms is not None:
|
|
246
|
+
log_entry.duration_ms = duration_ms
|
|
247
|
+
|
|
248
|
+
await log_entry.asave(update_fields=["status", "acks_received", "completed_at", "duration_ms"])
|
|
245
249
|
|
|
246
250
|
logger.info(
|
|
247
251
|
f"Centrifugo publish successful: {log_entry.message_id}",
|
|
@@ -400,6 +404,64 @@ class CentrifugoLogger:
|
|
|
400
404
|
extra={"message_id": getattr(log_entry, "message_id", "unknown")},
|
|
401
405
|
)
|
|
402
406
|
|
|
407
|
+
@staticmethod
|
|
408
|
+
async def mark_failed_async(
|
|
409
|
+
log_entry: Any,
|
|
410
|
+
error_code: str,
|
|
411
|
+
error_message: str,
|
|
412
|
+
duration_ms: int | None = None,
|
|
413
|
+
) -> None:
|
|
414
|
+
"""
|
|
415
|
+
Mark publish operation as failed (async version).
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
log_entry: CentrifugoLog instance
|
|
419
|
+
error_code: Error code
|
|
420
|
+
error_message: Error message
|
|
421
|
+
duration_ms: Duration in milliseconds
|
|
422
|
+
"""
|
|
423
|
+
if log_entry is None:
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
from ..models import CentrifugoLog
|
|
428
|
+
|
|
429
|
+
# ✅ Use Django 5.2+ async ORM instead of sync_to_async
|
|
430
|
+
log_entry.status = CentrifugoLog.StatusChoices.FAILED
|
|
431
|
+
log_entry.error_code = error_code
|
|
432
|
+
log_entry.error_message = error_message
|
|
433
|
+
log_entry.completed_at = timezone.now()
|
|
434
|
+
|
|
435
|
+
if duration_ms is not None:
|
|
436
|
+
log_entry.duration_ms = duration_ms
|
|
437
|
+
|
|
438
|
+
await log_entry.asave(
|
|
439
|
+
update_fields=[
|
|
440
|
+
"status",
|
|
441
|
+
"error_code",
|
|
442
|
+
"error_message",
|
|
443
|
+
"completed_at",
|
|
444
|
+
"duration_ms",
|
|
445
|
+
]
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
logger.error(
|
|
449
|
+
f"Centrifugo publish failed: {log_entry.message_id}",
|
|
450
|
+
extra={
|
|
451
|
+
"message_id": log_entry.message_id,
|
|
452
|
+
"channel": log_entry.channel,
|
|
453
|
+
"error_code": error_code,
|
|
454
|
+
"error_message": error_message,
|
|
455
|
+
"duration_ms": duration_ms,
|
|
456
|
+
},
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
except Exception as e:
|
|
460
|
+
logger.error(
|
|
461
|
+
f"Failed to mark Centrifugo log as failed: {e}",
|
|
462
|
+
extra={"message_id": getattr(log_entry, "message_id", "unknown")},
|
|
463
|
+
)
|
|
464
|
+
|
|
403
465
|
@staticmethod
|
|
404
466
|
async def mark_timeout_async(
|
|
405
467
|
log_entry: Any,
|
|
@@ -418,13 +480,27 @@ class CentrifugoLogger:
|
|
|
418
480
|
return
|
|
419
481
|
|
|
420
482
|
try:
|
|
421
|
-
from asgiref.sync import sync_to_async
|
|
422
483
|
from ..models import CentrifugoLog
|
|
423
484
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
485
|
+
# ✅ Use Django 5.2+ async ORM instead of sync_to_async
|
|
486
|
+
log_entry.status = CentrifugoLog.StatusChoices.TIMEOUT
|
|
487
|
+
log_entry.acks_received = acks_received
|
|
488
|
+
log_entry.error_code = "timeout"
|
|
489
|
+
log_entry.error_message = f"Timeout after {log_entry.ack_timeout}s"
|
|
490
|
+
log_entry.completed_at = timezone.now()
|
|
491
|
+
|
|
492
|
+
if duration_ms is not None:
|
|
493
|
+
log_entry.duration_ms = duration_ms
|
|
494
|
+
|
|
495
|
+
await log_entry.asave(
|
|
496
|
+
update_fields=[
|
|
497
|
+
"status",
|
|
498
|
+
"acks_received",
|
|
499
|
+
"error_code",
|
|
500
|
+
"error_message",
|
|
501
|
+
"completed_at",
|
|
502
|
+
"duration_ms",
|
|
503
|
+
]
|
|
428
504
|
)
|
|
429
505
|
|
|
430
506
|
logger.warning(
|
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
363
|
-
"""
|
|
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
|
-
|
|
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
|
|
@@ -13,6 +12,7 @@ from typing import Any, Dict
|
|
|
13
12
|
import httpx
|
|
14
13
|
import jwt
|
|
15
14
|
from django.conf import settings
|
|
15
|
+
from django.utils import timezone
|
|
16
16
|
from django_cfg.modules.django_logging import get_logger
|
|
17
17
|
from drf_spectacular.utils import extend_schema
|
|
18
18
|
from pydantic import BaseModel, Field
|
|
@@ -127,23 +127,21 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
127
127
|
},
|
|
128
128
|
)
|
|
129
129
|
@action(detail=False, methods=["post"], url_path="publish-test")
|
|
130
|
-
def publish_test(self, request):
|
|
130
|
+
async def publish_test(self, request):
|
|
131
131
|
"""
|
|
132
|
-
Publish test message via wrapper.
|
|
132
|
+
Publish test message via wrapper (ASYNC).
|
|
133
133
|
|
|
134
134
|
Proxies request to Centrifugo wrapper with ACK tracking support.
|
|
135
135
|
"""
|
|
136
136
|
try:
|
|
137
137
|
req_data = PublishTestRequest(**request.data)
|
|
138
138
|
|
|
139
|
-
# Call wrapper API
|
|
140
|
-
result =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
ack_timeout=req_data.ack_timeout,
|
|
146
|
-
)
|
|
139
|
+
# Call wrapper API (ASYNC - no asyncio.run()!)
|
|
140
|
+
result = await self._publish_to_wrapper(
|
|
141
|
+
channel=req_data.channel,
|
|
142
|
+
data=req_data.data,
|
|
143
|
+
wait_for_ack=req_data.wait_for_ack,
|
|
144
|
+
ack_timeout=req_data.ack_timeout,
|
|
147
145
|
)
|
|
148
146
|
|
|
149
147
|
response = PublishTestResponse(
|
|
@@ -180,20 +178,18 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
180
178
|
},
|
|
181
179
|
)
|
|
182
180
|
@action(detail=False, methods=["post"], url_path="send-ack")
|
|
183
|
-
def send_ack(self, request):
|
|
181
|
+
async def send_ack(self, request):
|
|
184
182
|
"""
|
|
185
|
-
Send manual ACK for message.
|
|
183
|
+
Send manual ACK for message (ASYNC).
|
|
186
184
|
|
|
187
185
|
Proxies ACK to wrapper for testing ACK flow.
|
|
188
186
|
"""
|
|
189
187
|
try:
|
|
190
188
|
req_data = ManualAckRequest(**request.data)
|
|
191
189
|
|
|
192
|
-
# Send ACK to wrapper
|
|
193
|
-
result =
|
|
194
|
-
|
|
195
|
-
message_id=req_data.message_id, client_id=req_data.client_id
|
|
196
|
-
)
|
|
190
|
+
# Send ACK to wrapper (ASYNC - no asyncio.run()!)
|
|
191
|
+
result = await self._send_ack_to_wrapper(
|
|
192
|
+
message_id=req_data.message_id, client_id=req_data.client_id
|
|
197
193
|
)
|
|
198
194
|
|
|
199
195
|
response = ManualAckResponse(
|
|
@@ -289,14 +285,23 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
289
285
|
|
|
290
286
|
# Mark as failed
|
|
291
287
|
if log_entry:
|
|
292
|
-
from asgiref.sync import sync_to_async
|
|
293
288
|
from ..models import CentrifugoLog
|
|
294
289
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
290
|
+
# ✅ Use Django 5.2+ async ORM instead of sync_to_async
|
|
291
|
+
log_entry.status = CentrifugoLog.StatusChoices.FAILED
|
|
292
|
+
log_entry.error_code = type(e).__name__
|
|
293
|
+
log_entry.error_message = str(e)
|
|
294
|
+
log_entry.completed_at = timezone.now()
|
|
295
|
+
log_entry.duration_ms = duration_ms
|
|
296
|
+
|
|
297
|
+
await log_entry.asave(
|
|
298
|
+
update_fields=[
|
|
299
|
+
"status",
|
|
300
|
+
"error_code",
|
|
301
|
+
"error_message",
|
|
302
|
+
"completed_at",
|
|
303
|
+
"duration_ms",
|
|
304
|
+
]
|
|
300
305
|
)
|
|
301
306
|
|
|
302
307
|
raise
|
|
@@ -334,9 +339,9 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
334
339
|
},
|
|
335
340
|
)
|
|
336
341
|
@action(detail=False, methods=["post"], url_path="publish-with-logging")
|
|
337
|
-
def publish_with_logging(self, request):
|
|
342
|
+
async def publish_with_logging(self, request):
|
|
338
343
|
"""
|
|
339
|
-
Publish message using CentrifugoClient with database logging.
|
|
344
|
+
Publish message using CentrifugoClient with database logging (ASYNC).
|
|
340
345
|
|
|
341
346
|
This endpoint uses the production CentrifugoClient which logs all
|
|
342
347
|
publishes to the database (CentrifugoLog model).
|
|
@@ -347,25 +352,24 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
347
352
|
# Use CentrifugoClient for publishing
|
|
348
353
|
client = CentrifugoClient()
|
|
349
354
|
|
|
350
|
-
# Publish message
|
|
351
|
-
|
|
352
|
-
client.publish_with_ack(
|
|
355
|
+
# Publish message (ASYNC - no asyncio.run()!)
|
|
356
|
+
if req_data.wait_for_ack:
|
|
357
|
+
result = await client.publish_with_ack(
|
|
353
358
|
channel=req_data.channel,
|
|
354
359
|
data=req_data.data,
|
|
355
|
-
ack_timeout=req_data.ack_timeout
|
|
360
|
+
ack_timeout=req_data.ack_timeout,
|
|
356
361
|
user=request.user if request.user.is_authenticated else None,
|
|
357
362
|
caller_ip=request.META.get("REMOTE_ADDR"),
|
|
358
363
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
|
359
364
|
)
|
|
360
|
-
|
|
361
|
-
|
|
365
|
+
else:
|
|
366
|
+
result = await client.publish(
|
|
362
367
|
channel=req_data.channel,
|
|
363
368
|
data=req_data.data,
|
|
364
369
|
user=request.user if request.user.is_authenticated else None,
|
|
365
370
|
caller_ip=request.META.get("REMOTE_ADDR"),
|
|
366
371
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
|
367
372
|
)
|
|
368
|
-
)
|
|
369
373
|
|
|
370
374
|
# Convert PublishResponse to dict
|
|
371
375
|
response_data = {
|
|
@@ -386,17 +390,17 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
386
390
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
387
391
|
)
|
|
388
392
|
|
|
389
|
-
def
|
|
390
|
-
"""
|
|
393
|
+
async def cleanup(self):
|
|
394
|
+
"""
|
|
395
|
+
Explicit async cleanup method for HTTP client.
|
|
396
|
+
|
|
397
|
+
Note: Django handles ViewSet lifecycle automatically.
|
|
398
|
+
This method is provided for explicit cleanup if needed,
|
|
399
|
+
but httpx.AsyncClient will be garbage collected normally.
|
|
400
|
+
"""
|
|
391
401
|
if self._http_client:
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
402
|
+
await self._http_client.aclose()
|
|
403
|
+
self._http_client = None
|
|
400
404
|
|
|
401
405
|
|
|
402
406
|
__all__ = ["CentrifugoTestingAPIViewSet"]
|
|
@@ -5,13 +5,14 @@ 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
|
+
from django.utils import timezone
|
|
15
16
|
from django.utils.decorators import method_decorator
|
|
16
17
|
from django.views import View
|
|
17
18
|
from django.views.decorators.csrf import csrf_exempt
|
|
@@ -55,15 +56,19 @@ class PublishResponse(BaseModel):
|
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
@method_decorator(csrf_exempt, name='dispatch')
|
|
59
|
+
@method_decorator(transaction.non_atomic_requests, name='dispatch')
|
|
58
60
|
class PublishWrapperView(View):
|
|
59
61
|
"""
|
|
60
|
-
Centrifugo publish wrapper endpoint.
|
|
62
|
+
Centrifugo publish wrapper endpoint (ASYNC).
|
|
61
63
|
|
|
62
64
|
Provides /api/publish endpoint that:
|
|
63
65
|
- Accepts publish requests from CentrifugoClient
|
|
64
66
|
- Logs to database (CentrifugoLog)
|
|
65
67
|
- Proxies to Centrifugo HTTP API
|
|
66
68
|
- Returns publish result with ACK tracking
|
|
69
|
+
|
|
70
|
+
NOTE: This is an async view for proper async/await handling.
|
|
71
|
+
Using asyncio.run() in sync views causes event loop conflicts.
|
|
67
72
|
"""
|
|
68
73
|
|
|
69
74
|
def __init__(self, *args, **kwargs):
|
|
@@ -169,21 +174,30 @@ class PublishWrapperView(View):
|
|
|
169
174
|
|
|
170
175
|
# Mark as failed
|
|
171
176
|
if log_entry:
|
|
172
|
-
from asgiref.sync import sync_to_async
|
|
173
177
|
from ..models import CentrifugoLog
|
|
174
178
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
# ✅ Use Django 5.2+ async ORM instead of sync_to_async
|
|
180
|
+
log_entry.status = CentrifugoLog.StatusChoices.FAILED
|
|
181
|
+
log_entry.error_code = type(e).__name__
|
|
182
|
+
log_entry.error_message = str(e)
|
|
183
|
+
log_entry.completed_at = timezone.now()
|
|
184
|
+
log_entry.duration_ms = duration_ms
|
|
185
|
+
|
|
186
|
+
await log_entry.asave(
|
|
187
|
+
update_fields=[
|
|
188
|
+
"status",
|
|
189
|
+
"error_code",
|
|
190
|
+
"error_message",
|
|
191
|
+
"completed_at",
|
|
192
|
+
"duration_ms",
|
|
193
|
+
]
|
|
180
194
|
)
|
|
181
195
|
|
|
182
196
|
raise
|
|
183
197
|
|
|
184
|
-
def post(self, request):
|
|
198
|
+
async def post(self, request):
|
|
185
199
|
"""
|
|
186
|
-
Handle POST /api/publish request.
|
|
200
|
+
Handle POST /api/publish request (ASYNC).
|
|
187
201
|
|
|
188
202
|
Request body:
|
|
189
203
|
{
|
|
@@ -213,15 +227,13 @@ class PublishWrapperView(View):
|
|
|
213
227
|
# Generate message ID if not provided
|
|
214
228
|
message_id = req_data.message_id or str(uuid.uuid4())
|
|
215
229
|
|
|
216
|
-
# Publish to Centrifugo
|
|
217
|
-
result =
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
message_id=message_id,
|
|
224
|
-
)
|
|
230
|
+
# Publish to Centrifugo (ASYNC - no asyncio.run()!)
|
|
231
|
+
result = await self._publish_to_centrifugo(
|
|
232
|
+
channel=req_data.channel,
|
|
233
|
+
data=req_data.data,
|
|
234
|
+
wait_for_ack=req_data.wait_for_ack,
|
|
235
|
+
ack_timeout=req_data.ack_timeout,
|
|
236
|
+
message_id=message_id,
|
|
225
237
|
)
|
|
226
238
|
|
|
227
239
|
response = PublishResponse(**result)
|
|
@@ -241,17 +253,17 @@ class PublishWrapperView(View):
|
|
|
241
253
|
status=500,
|
|
242
254
|
)
|
|
243
255
|
|
|
244
|
-
def
|
|
245
|
-
"""
|
|
256
|
+
async def cleanup(self):
|
|
257
|
+
"""
|
|
258
|
+
Explicit async cleanup method for HTTP client.
|
|
259
|
+
|
|
260
|
+
Note: Django handles View lifecycle automatically.
|
|
261
|
+
This method is provided for explicit cleanup if needed,
|
|
262
|
+
but httpx.AsyncClient will be garbage collected normally.
|
|
263
|
+
"""
|
|
246
264
|
if self._http_client:
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
265
|
+
await self._http_client.aclose()
|
|
266
|
+
self._http_client = None
|
|
255
267
|
|
|
256
268
|
|
|
257
269
|
__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
|
-
#
|
|
174
|
-
superuser = await
|
|
175
|
-
|
|
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
|
-
#
|
|
191
|
-
api_key_obj = await
|
|
192
|
-
|
|
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 (
|
|
197
|
-
await
|
|
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()
|