django-cfg 1.5.8__py3-none-any.whl → 1.5.20__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/api/commands/serializers.py +152 -0
- django_cfg/apps/api/commands/views.py +32 -0
- django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
- django_cfg/apps/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
- django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
- django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
- django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
- django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
- django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
- django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
- django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
- django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
- django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
- django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
- django_cfg/apps/integrations/grpc/admin/config.py +113 -9
- django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
- django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
- django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
- django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
- django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
- django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
- django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
- django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
- django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
- django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
- django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
- django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
- django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
- django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
- django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
- django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
- django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
- django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
- django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
- django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
- django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
- django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
- django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
- django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
- django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
- django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
- django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
- django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
- django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
- django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
- django_cfg/apps/integrations/grpc/urls.py +8 -0
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
- django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
- django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
- django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
- django_cfg/apps/integrations/grpc/views/charts.py +21 -14
- django_cfg/apps/integrations/grpc/views/config.py +8 -6
- django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
- django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
- django_cfg/apps/integrations/grpc/views/services.py +30 -21
- django_cfg/apps/integrations/grpc/views/testing.py +45 -43
- django_cfg/apps/integrations/rq/views/jobs.py +19 -9
- django_cfg/apps/integrations/rq/views/schedule.py +7 -3
- django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
- django_cfg/apps/system/dashboard/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
- django_cfg/apps/system/frontend/views.py +87 -6
- django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
- django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
- django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
- django_cfg/config.py +33 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
- django_cfg/management/commands/check_endpoints.py +2 -2
- django_cfg/management/commands/check_settings.py +3 -10
- django_cfg/management/commands/clear_constance.py +3 -10
- django_cfg/management/commands/create_token.py +4 -11
- django_cfg/management/commands/list_urls.py +4 -10
- django_cfg/management/commands/migrate_all.py +18 -12
- django_cfg/management/commands/migrator.py +4 -11
- django_cfg/management/commands/script.py +4 -10
- django_cfg/management/commands/show_config.py +8 -16
- django_cfg/management/commands/show_urls.py +5 -11
- django_cfg/management/commands/superuser.py +4 -11
- django_cfg/management/commands/tree.py +5 -10
- django_cfg/management/utils/README.md +402 -0
- django_cfg/management/utils/__init__.py +29 -0
- django_cfg/management/utils/mixins.py +176 -0
- django_cfg/middleware/pagination.py +53 -54
- django_cfg/models/api/grpc/__init__.py +15 -21
- django_cfg/models/api/grpc/config.py +155 -73
- django_cfg/models/ngrok/config.py +7 -6
- django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
- django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +126 -30
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
- django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
- django_cfg/modules/django_email/management/commands/test_email.py +4 -10
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
- django_cfg/modules/django_unfold/navigation.py +6 -18
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/modules.py +1 -4
- django_cfg/requirements.txt +52 -0
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
- django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -32,23 +32,6 @@ logger = get_logger("centrifugo.testing_api")
|
|
|
32
32
|
# ========================================================================
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
class ConnectionTokenRequest(BaseModel):
|
|
36
|
-
"""Request model for connection token generation."""
|
|
37
|
-
|
|
38
|
-
user_id: str = Field(..., description="User ID for the connection")
|
|
39
|
-
channels: list[str] = Field(
|
|
40
|
-
default_factory=list, description="List of channels to authorize"
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class ConnectionTokenResponse(BaseModel):
|
|
45
|
-
"""Response model for connection token."""
|
|
46
|
-
|
|
47
|
-
token: str = Field(..., description="JWT token for WebSocket connection")
|
|
48
|
-
centrifugo_url: str = Field(..., description="Centrifugo WebSocket URL")
|
|
49
|
-
expires_at: str = Field(..., description="Token expiration time (ISO 8601)")
|
|
50
|
-
|
|
51
|
-
|
|
52
35
|
class PublishTestRequest(BaseModel):
|
|
53
36
|
"""Request model for test message publishing."""
|
|
54
37
|
|
|
@@ -132,68 +115,6 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
132
115
|
|
|
133
116
|
return self._http_client
|
|
134
117
|
|
|
135
|
-
@extend_schema(
|
|
136
|
-
tags=["Centrifugo Testing"],
|
|
137
|
-
summary="Generate connection token",
|
|
138
|
-
description="Generate JWT token for WebSocket connection to Centrifugo.",
|
|
139
|
-
request=ConnectionTokenRequest,
|
|
140
|
-
responses={
|
|
141
|
-
200: ConnectionTokenResponse,
|
|
142
|
-
400: {"description": "Invalid request"},
|
|
143
|
-
500: {"description": "Server error"},
|
|
144
|
-
},
|
|
145
|
-
)
|
|
146
|
-
@action(detail=False, methods=["post"], url_path="connection-token")
|
|
147
|
-
def connection_token(self, request):
|
|
148
|
-
"""
|
|
149
|
-
Generate JWT token for WebSocket connection.
|
|
150
|
-
|
|
151
|
-
Returns token that can be used to connect to Centrifugo from browser.
|
|
152
|
-
"""
|
|
153
|
-
try:
|
|
154
|
-
config = get_centrifugo_config()
|
|
155
|
-
if not config:
|
|
156
|
-
return Response(
|
|
157
|
-
{"error": "Centrifugo not configured"},
|
|
158
|
-
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
# Parse request
|
|
162
|
-
req_data = ConnectionTokenRequest(**request.data)
|
|
163
|
-
|
|
164
|
-
# Generate JWT token
|
|
165
|
-
now = int(time.time())
|
|
166
|
-
exp = now + 3600 # 1 hour
|
|
167
|
-
|
|
168
|
-
payload = {
|
|
169
|
-
"sub": req_data.user_id,
|
|
170
|
-
"exp": exp,
|
|
171
|
-
"iat": now,
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
# Add channels if provided
|
|
175
|
-
if req_data.channels:
|
|
176
|
-
payload["channels"] = req_data.channels
|
|
177
|
-
|
|
178
|
-
# Use HMAC secret from config or Django SECRET_KEY
|
|
179
|
-
secret = config.centrifugo_token_hmac_secret or settings.SECRET_KEY
|
|
180
|
-
|
|
181
|
-
token = jwt.encode(payload, secret, algorithm="HS256")
|
|
182
|
-
|
|
183
|
-
response = ConnectionTokenResponse(
|
|
184
|
-
token=token,
|
|
185
|
-
centrifugo_url=config.centrifugo_url,
|
|
186
|
-
expires_at=datetime.utcfromtimestamp(exp).isoformat() + "Z",
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
return Response(response.model_dump())
|
|
190
|
-
|
|
191
|
-
except Exception as e:
|
|
192
|
-
logger.error(f"Failed to generate connection token: {e}", exc_info=True)
|
|
193
|
-
return Response(
|
|
194
|
-
{"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
195
|
-
)
|
|
196
|
-
|
|
197
118
|
@extend_schema(
|
|
198
119
|
tags=["Centrifugo Testing"],
|
|
199
120
|
summary="Publish test message",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Token API
|
|
3
|
+
|
|
4
|
+
Provides endpoint for generating Centrifugo JWT tokens with user permissions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from drf_spectacular.utils import extend_schema
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
from rest_framework import status, viewsets
|
|
12
|
+
from rest_framework.decorators import action
|
|
13
|
+
from rest_framework.response import Response
|
|
14
|
+
from rest_framework.permissions import IsAuthenticated
|
|
15
|
+
|
|
16
|
+
from ..services import generate_centrifugo_token
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ========================================================================
|
|
22
|
+
# Response Models
|
|
23
|
+
# ========================================================================
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConnectionTokenResponse(BaseModel):
|
|
27
|
+
"""Response model for Centrifugo connection token."""
|
|
28
|
+
|
|
29
|
+
token: str = Field(..., description="JWT token for Centrifugo connection")
|
|
30
|
+
centrifugo_url: str = Field(..., description="Centrifugo WebSocket URL")
|
|
31
|
+
expires_at: str = Field(..., description="Token expiration time (ISO 8601)")
|
|
32
|
+
channels: list[str] = Field(..., description="List of allowed channels")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CentrifugoTokenViewSet(viewsets.ViewSet):
|
|
36
|
+
"""
|
|
37
|
+
Centrifugo Token API ViewSet.
|
|
38
|
+
|
|
39
|
+
Provides endpoint for authenticated users to get Centrifugo JWT token
|
|
40
|
+
with their allowed channels based on permissions.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
permission_classes = [IsAuthenticated]
|
|
44
|
+
|
|
45
|
+
@extend_schema(
|
|
46
|
+
tags=["Centrifugo Auth"],
|
|
47
|
+
summary="Get Centrifugo connection token",
|
|
48
|
+
description=(
|
|
49
|
+
"Generate JWT token for WebSocket connection to Centrifugo. "
|
|
50
|
+
"Token includes user's allowed channels based on their permissions. "
|
|
51
|
+
"Requires authentication."
|
|
52
|
+
),
|
|
53
|
+
responses={
|
|
54
|
+
200: ConnectionTokenResponse,
|
|
55
|
+
401: {"description": "Unauthorized - authentication required"},
|
|
56
|
+
500: {"description": "Server error"},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
@action(detail=False, methods=["get"], url_path="token")
|
|
60
|
+
def get_token(self, request):
|
|
61
|
+
"""
|
|
62
|
+
Get Centrifugo connection token for authenticated user.
|
|
63
|
+
|
|
64
|
+
Returns JWT token with user's allowed channels that can be used
|
|
65
|
+
to connect to Centrifugo WebSocket.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
user = request.user
|
|
69
|
+
|
|
70
|
+
# Generate token with user's channels
|
|
71
|
+
token_data = generate_centrifugo_token(user)
|
|
72
|
+
|
|
73
|
+
logger.debug(
|
|
74
|
+
f"Generated Centrifugo token for user {user.email} (ID: {user.id}) "
|
|
75
|
+
f"with {len(token_data['channels'])} channels: {token_data['channels']}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Format expires_at as ISO 8601 string
|
|
79
|
+
return Response({
|
|
80
|
+
"token": token_data["token"],
|
|
81
|
+
"centrifugo_url": token_data["centrifugo_url"],
|
|
82
|
+
"expires_at": token_data["expires_at"].isoformat() + "Z",
|
|
83
|
+
"channels": token_data["channels"],
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
except ValueError as e:
|
|
87
|
+
# Centrifugo not configured or disabled
|
|
88
|
+
logger.warning(f"Centrifugo token generation failed: {e}")
|
|
89
|
+
return Response(
|
|
90
|
+
{"error": str(e)},
|
|
91
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
92
|
+
)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Failed to generate Centrifugo token: {e}", exc_info=True)
|
|
95
|
+
return Response(
|
|
96
|
+
{"error": "Failed to generate token"},
|
|
97
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
__all__ = ["CentrifugoTokenViewSet"]
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Wrapper API.
|
|
3
|
+
|
|
4
|
+
Provides /api/publish endpoint that acts as a proxy to Centrifugo
|
|
5
|
+
with ACK tracking and database logging.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Any, Dict
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from django.http import JsonResponse
|
|
15
|
+
from django.utils.decorators import method_decorator
|
|
16
|
+
from django.views import View
|
|
17
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
18
|
+
from django_cfg.modules.django_logging import get_logger
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from ..services import get_centrifugo_config
|
|
22
|
+
from ..services.logging import CentrifugoLogger
|
|
23
|
+
|
|
24
|
+
logger = get_logger("centrifugo.wrapper")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ========================================================================
|
|
28
|
+
# Request/Response Models
|
|
29
|
+
# ========================================================================
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PublishRequest(BaseModel):
|
|
33
|
+
"""Request model for publish endpoint."""
|
|
34
|
+
|
|
35
|
+
channel: str = Field(..., description="Target channel name")
|
|
36
|
+
data: Dict[str, Any] = Field(..., description="Message data")
|
|
37
|
+
wait_for_ack: bool = Field(default=False, description="Wait for client ACK")
|
|
38
|
+
ack_timeout: int = Field(default=10, description="ACK timeout in seconds")
|
|
39
|
+
message_id: str | None = Field(default=None, description="Optional message ID")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PublishResponse(BaseModel):
|
|
43
|
+
"""Response model for publish endpoint."""
|
|
44
|
+
|
|
45
|
+
published: bool = Field(..., description="Whether message was published")
|
|
46
|
+
message_id: str = Field(..., description="Unique message ID")
|
|
47
|
+
channel: str = Field(..., description="Target channel")
|
|
48
|
+
delivered: bool = Field(default=False, description="Whether message was delivered")
|
|
49
|
+
acks_received: int = Field(default=0, description="Number of ACKs received")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ========================================================================
|
|
53
|
+
# Wrapper View
|
|
54
|
+
# ========================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@method_decorator(csrf_exempt, name='dispatch')
|
|
58
|
+
class PublishWrapperView(View):
|
|
59
|
+
"""
|
|
60
|
+
Centrifugo publish wrapper endpoint.
|
|
61
|
+
|
|
62
|
+
Provides /api/publish endpoint that:
|
|
63
|
+
- Accepts publish requests from CentrifugoClient
|
|
64
|
+
- Logs to database (CentrifugoLog)
|
|
65
|
+
- Proxies to Centrifugo HTTP API
|
|
66
|
+
- Returns publish result with ACK tracking
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, *args, **kwargs):
|
|
70
|
+
super().__init__(*args, **kwargs)
|
|
71
|
+
self._http_client = None
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def http_client(self) -> httpx.AsyncClient:
|
|
75
|
+
"""Get or create HTTP client for Centrifugo API calls."""
|
|
76
|
+
if self._http_client is None:
|
|
77
|
+
config = get_centrifugo_config()
|
|
78
|
+
if not config:
|
|
79
|
+
raise ValueError("Centrifugo not configured")
|
|
80
|
+
|
|
81
|
+
headers = {"Content-Type": "application/json"}
|
|
82
|
+
|
|
83
|
+
# Add Centrifugo API key for server-to-server auth
|
|
84
|
+
if config.centrifugo_api_key:
|
|
85
|
+
headers["Authorization"] = f"apikey {config.centrifugo_api_key}"
|
|
86
|
+
|
|
87
|
+
# Use Centrifugo API URL (not wrapper URL)
|
|
88
|
+
base_url = config.centrifugo_api_url.rstrip("/api").rstrip("/")
|
|
89
|
+
|
|
90
|
+
self._http_client = httpx.AsyncClient(
|
|
91
|
+
base_url=base_url,
|
|
92
|
+
headers=headers,
|
|
93
|
+
timeout=httpx.Timeout(config.http_timeout),
|
|
94
|
+
verify=config.verify_ssl,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return self._http_client
|
|
98
|
+
|
|
99
|
+
async def _publish_to_centrifugo(
|
|
100
|
+
self, channel: str, data: Dict[str, Any], wait_for_ack: bool, ack_timeout: int, message_id: str
|
|
101
|
+
) -> Dict[str, Any]:
|
|
102
|
+
"""
|
|
103
|
+
Publish message to Centrifugo API.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
channel: Target channel
|
|
107
|
+
data: Message data
|
|
108
|
+
wait_for_ack: Whether to wait for ACK
|
|
109
|
+
ack_timeout: ACK timeout in seconds
|
|
110
|
+
message_id: Message ID
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Publish result dict
|
|
114
|
+
"""
|
|
115
|
+
start_time = time.time()
|
|
116
|
+
|
|
117
|
+
# Create log entry
|
|
118
|
+
log_entry = await CentrifugoLogger.create_log_async(
|
|
119
|
+
message_id=message_id,
|
|
120
|
+
channel=channel,
|
|
121
|
+
data=data,
|
|
122
|
+
wait_for_ack=wait_for_ack,
|
|
123
|
+
ack_timeout=ack_timeout if wait_for_ack else None,
|
|
124
|
+
is_notification=True,
|
|
125
|
+
user=None, # Can extract from request if needed
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
# Centrifugo API format: POST /api with method in body
|
|
130
|
+
payload = {
|
|
131
|
+
"method": "publish",
|
|
132
|
+
"params": {
|
|
133
|
+
"channel": channel,
|
|
134
|
+
"data": data,
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
response = await self.http_client.post("/api", json=payload)
|
|
139
|
+
response.raise_for_status()
|
|
140
|
+
result = response.json()
|
|
141
|
+
|
|
142
|
+
# Check for Centrifugo error
|
|
143
|
+
if "error" in result and result["error"]:
|
|
144
|
+
raise Exception(f"Centrifugo error: {result['error']}")
|
|
145
|
+
|
|
146
|
+
# Calculate duration
|
|
147
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
148
|
+
|
|
149
|
+
# Mark as success
|
|
150
|
+
if log_entry:
|
|
151
|
+
await CentrifugoLogger.mark_success_async(
|
|
152
|
+
log_entry,
|
|
153
|
+
acks_received=0, # ACK tracking would be implemented separately
|
|
154
|
+
duration_ms=duration_ms,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Return wrapper-compatible response
|
|
158
|
+
return {
|
|
159
|
+
"published": True,
|
|
160
|
+
"message_id": message_id,
|
|
161
|
+
"channel": channel,
|
|
162
|
+
"acks_received": 0,
|
|
163
|
+
"delivered": True, # Centrifugo confirms publish, not delivery
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
# Calculate duration
|
|
168
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
169
|
+
|
|
170
|
+
# Mark as failed
|
|
171
|
+
if log_entry:
|
|
172
|
+
from asgiref.sync import sync_to_async
|
|
173
|
+
from ..models import CentrifugoLog
|
|
174
|
+
|
|
175
|
+
await sync_to_async(CentrifugoLog.objects.mark_failed)(
|
|
176
|
+
log_instance=log_entry,
|
|
177
|
+
error_code=type(e).__name__,
|
|
178
|
+
error_message=str(e),
|
|
179
|
+
duration_ms=duration_ms,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
raise
|
|
183
|
+
|
|
184
|
+
def post(self, request):
|
|
185
|
+
"""
|
|
186
|
+
Handle POST /api/publish request.
|
|
187
|
+
|
|
188
|
+
Request body:
|
|
189
|
+
{
|
|
190
|
+
"channel": "test#demo",
|
|
191
|
+
"data": {"key": "value"},
|
|
192
|
+
"wait_for_ack": false,
|
|
193
|
+
"ack_timeout": 10,
|
|
194
|
+
"message_id": "optional-uuid"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
Response:
|
|
198
|
+
{
|
|
199
|
+
"published": true,
|
|
200
|
+
"message_id": "uuid",
|
|
201
|
+
"channel": "test#demo",
|
|
202
|
+
"delivered": true,
|
|
203
|
+
"acks_received": 0
|
|
204
|
+
}
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
import json
|
|
208
|
+
|
|
209
|
+
# Parse request body
|
|
210
|
+
body = json.loads(request.body)
|
|
211
|
+
req_data = PublishRequest(**body)
|
|
212
|
+
|
|
213
|
+
# Generate message ID if not provided
|
|
214
|
+
message_id = req_data.message_id or str(uuid.uuid4())
|
|
215
|
+
|
|
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
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
response = PublishResponse(**result)
|
|
228
|
+
return JsonResponse(response.model_dump(), status=200)
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.error(f"Failed to publish via wrapper: {e}", exc_info=True)
|
|
232
|
+
return JsonResponse(
|
|
233
|
+
{
|
|
234
|
+
"published": False,
|
|
235
|
+
"message_id": "",
|
|
236
|
+
"channel": body.get("channel", "") if "body" in locals() else "",
|
|
237
|
+
"delivered": False,
|
|
238
|
+
"acks_received": 0,
|
|
239
|
+
"error": str(e),
|
|
240
|
+
},
|
|
241
|
+
status=500,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def __del__(self):
|
|
245
|
+
"""Cleanup HTTP client on deletion."""
|
|
246
|
+
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
|
+
|
|
256
|
+
|
|
257
|
+
__all__ = ["PublishWrapperView"]
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
Admin interface for gRPC app.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from .config import grpcrequestlog_config
|
|
5
|
+
from .config import grpcapikey_config, grpcrequestlog_config, grpcserverstatus_config
|
|
6
|
+
from .grpc_api_key import GrpcApiKeyAdmin
|
|
6
7
|
from .grpc_request_log import GRPCRequestLogAdmin
|
|
8
|
+
from .grpc_server_status import GRPCServerStatusAdmin
|
|
7
9
|
|
|
8
10
|
__all__ = [
|
|
11
|
+
"GrpcApiKeyAdmin",
|
|
9
12
|
"GRPCRequestLogAdmin",
|
|
13
|
+
"GRPCServerStatusAdmin",
|
|
14
|
+
"grpcapikey_config",
|
|
10
15
|
"grpcrequestlog_config",
|
|
16
|
+
"grpcserverstatus_config",
|
|
11
17
|
]
|
|
@@ -7,31 +7,33 @@ Declarative AdminConfig using PydanticAdmin patterns.
|
|
|
7
7
|
from django_cfg.modules.django_admin import (
|
|
8
8
|
AdminConfig,
|
|
9
9
|
BadgeField,
|
|
10
|
+
BooleanField,
|
|
10
11
|
DateTimeField,
|
|
12
|
+
FieldsetConfig,
|
|
11
13
|
Icons,
|
|
14
|
+
TextField,
|
|
12
15
|
UserField,
|
|
13
16
|
)
|
|
14
17
|
|
|
15
|
-
from ..models import GRPCRequestLog, GRPCServerStatus
|
|
18
|
+
from ..models import GRPCRequestLog, GRPCServerStatus, GrpcApiKey
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
# Declarative configuration for GRPCRequestLog
|
|
19
22
|
grpcrequestlog_config = AdminConfig(
|
|
20
23
|
model=GRPCRequestLog,
|
|
21
24
|
# Performance optimization
|
|
22
|
-
select_related=["user"],
|
|
25
|
+
select_related=["user", "api_key"],
|
|
23
26
|
|
|
24
27
|
# List display
|
|
25
28
|
list_display=[
|
|
26
|
-
"full_method",
|
|
27
29
|
"service_badge",
|
|
28
30
|
"method_badge",
|
|
29
31
|
"status",
|
|
30
32
|
"grpc_status_code_display",
|
|
31
33
|
"user",
|
|
34
|
+
"api_key_display",
|
|
32
35
|
"duration_display",
|
|
33
36
|
"created_at",
|
|
34
|
-
"completed_at"
|
|
35
37
|
],
|
|
36
38
|
|
|
37
39
|
# Auto-generated display methods
|
|
@@ -54,7 +56,7 @@ grpcrequestlog_config = AdminConfig(
|
|
|
54
56
|
DateTimeField(name="completed_at", title="Completed", ordering="completed_at"),
|
|
55
57
|
],
|
|
56
58
|
# Filters
|
|
57
|
-
list_filter=["status", "grpc_status_code", "service_name", "method_name", "is_authenticated", "created_at"],
|
|
59
|
+
list_filter=["status", "grpc_status_code", "service_name", "method_name", "is_authenticated", "api_key", "created_at"],
|
|
58
60
|
search_fields=[
|
|
59
61
|
"request_id",
|
|
60
62
|
"service_name",
|
|
@@ -62,11 +64,13 @@ grpcrequestlog_config = AdminConfig(
|
|
|
62
64
|
"full_method",
|
|
63
65
|
"user__username",
|
|
64
66
|
"user__email",
|
|
67
|
+
"api_key__name",
|
|
68
|
+
"api_key__key",
|
|
65
69
|
"error_message",
|
|
66
70
|
"client_ip",
|
|
67
71
|
],
|
|
68
|
-
# Autocomplete for user
|
|
69
|
-
autocomplete_fields=["user"],
|
|
72
|
+
# Autocomplete for user and api_key fields
|
|
73
|
+
autocomplete_fields=["user", "api_key"],
|
|
70
74
|
# Readonly fields
|
|
71
75
|
readonly_fields=[
|
|
72
76
|
"id",
|
|
@@ -94,7 +98,7 @@ grpcserverstatus_config = AdminConfig(
|
|
|
94
98
|
list_display=[
|
|
95
99
|
"instance_id",
|
|
96
100
|
"address",
|
|
97
|
-
"
|
|
101
|
+
"status",
|
|
98
102
|
"pid",
|
|
99
103
|
"hostname",
|
|
100
104
|
"uptime_display",
|
|
@@ -141,6 +145,11 @@ grpcserverstatus_config = AdminConfig(
|
|
|
141
145
|
"updated_at",
|
|
142
146
|
"uptime_display",
|
|
143
147
|
"is_running",
|
|
148
|
+
"server_config_display",
|
|
149
|
+
"process_info_display",
|
|
150
|
+
"registered_services_display",
|
|
151
|
+
"error_display",
|
|
152
|
+
"lifecycle_display",
|
|
144
153
|
],
|
|
145
154
|
|
|
146
155
|
# Date hierarchy
|
|
@@ -154,4 +163,99 @@ grpcserverstatus_config = AdminConfig(
|
|
|
154
163
|
)
|
|
155
164
|
|
|
156
165
|
|
|
157
|
-
|
|
166
|
+
# Declarative configuration for GrpcApiKey
|
|
167
|
+
grpcapikey_config = AdminConfig(
|
|
168
|
+
model=GrpcApiKey,
|
|
169
|
+
|
|
170
|
+
# Performance optimization
|
|
171
|
+
select_related=["user", "created_by"],
|
|
172
|
+
|
|
173
|
+
# List display
|
|
174
|
+
list_display=[
|
|
175
|
+
"status_indicator",
|
|
176
|
+
"name",
|
|
177
|
+
"key_type",
|
|
178
|
+
"user",
|
|
179
|
+
"masked_key_display",
|
|
180
|
+
"request_count_display",
|
|
181
|
+
"last_used_at",
|
|
182
|
+
"expires_display",
|
|
183
|
+
"created_at",
|
|
184
|
+
],
|
|
185
|
+
|
|
186
|
+
# Auto-generated display methods
|
|
187
|
+
display_fields=[
|
|
188
|
+
TextField(name="name", title="Name", ordering="name"),
|
|
189
|
+
BadgeField(
|
|
190
|
+
name="key_type",
|
|
191
|
+
title="Type",
|
|
192
|
+
label_map={
|
|
193
|
+
"service": "info",
|
|
194
|
+
"cli": "primary",
|
|
195
|
+
"webhook": "warning",
|
|
196
|
+
"internal": "secondary",
|
|
197
|
+
"development": "danger",
|
|
198
|
+
},
|
|
199
|
+
),
|
|
200
|
+
UserField(name="user", title="User", header=True, ordering="user__username"),
|
|
201
|
+
DateTimeField(name="last_used_at", title="Last Used", ordering="last_used_at"),
|
|
202
|
+
DateTimeField(name="created_at", title="Created", ordering="created_at"),
|
|
203
|
+
],
|
|
204
|
+
|
|
205
|
+
# Filters
|
|
206
|
+
list_filter=["is_active", "key_type", "created_at", "expires_at", "user"],
|
|
207
|
+
search_fields=["name", "description", "user__username", "user__email", "key"],
|
|
208
|
+
|
|
209
|
+
# Readonly fields
|
|
210
|
+
readonly_fields=[
|
|
211
|
+
"key_display",
|
|
212
|
+
"masked_key",
|
|
213
|
+
"request_count",
|
|
214
|
+
"last_used_at",
|
|
215
|
+
"created_at",
|
|
216
|
+
"updated_at",
|
|
217
|
+
"created_by",
|
|
218
|
+
],
|
|
219
|
+
|
|
220
|
+
# Fieldsets
|
|
221
|
+
fieldsets=[
|
|
222
|
+
FieldsetConfig(
|
|
223
|
+
title="Basic Information",
|
|
224
|
+
fields=["name", "description", "key_type", "is_active"],
|
|
225
|
+
),
|
|
226
|
+
FieldsetConfig(
|
|
227
|
+
title="API Key",
|
|
228
|
+
fields=["key_display", "masked_key"],
|
|
229
|
+
),
|
|
230
|
+
FieldsetConfig(
|
|
231
|
+
title="User Association",
|
|
232
|
+
fields=["user", "created_by"],
|
|
233
|
+
),
|
|
234
|
+
FieldsetConfig(
|
|
235
|
+
title="Expiration",
|
|
236
|
+
fields=["expires_at"],
|
|
237
|
+
),
|
|
238
|
+
FieldsetConfig(
|
|
239
|
+
title="Usage Statistics",
|
|
240
|
+
fields=["request_count", "last_used_at"],
|
|
241
|
+
collapsed=True,
|
|
242
|
+
),
|
|
243
|
+
FieldsetConfig(
|
|
244
|
+
title="Timestamps",
|
|
245
|
+
fields=["created_at", "updated_at"],
|
|
246
|
+
collapsed=True,
|
|
247
|
+
),
|
|
248
|
+
],
|
|
249
|
+
|
|
250
|
+
# Autocomplete for user field
|
|
251
|
+
autocomplete_fields=["user"],
|
|
252
|
+
|
|
253
|
+
# Ordering
|
|
254
|
+
ordering=["-created_at"],
|
|
255
|
+
|
|
256
|
+
# Per page
|
|
257
|
+
list_per_page=50,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
__all__ = ["grpcrequestlog_config", "grpcserverstatus_config", "grpcapikey_config"]
|