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.
- 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 +47 -0
- django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -37
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +25 -23
- 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 +21 -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} +215 -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/groups/manager.py +25 -18
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +0 -39
- 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.29.dist-info}/METADATA +75 -5
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/RECORD +87 -59
- 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.29.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.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,
|
|
@@ -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 =
|
|
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
|
|
@@ -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 =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 =
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
361
|
-
|
|
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
|
|
390
|
-
"""
|
|
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
|
-
|
|
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
|
|
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 =
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
245
|
-
"""
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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()
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
706
|
-
|
|
707
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
814
|
-
|
|
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
|
|