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.

Files changed (159) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/commands/serializers.py +152 -0
  3. django_cfg/apps/api/commands/views.py +32 -0
  4. django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
  5. django_cfg/apps/business/accounts/serializers/profile.py +42 -0
  6. django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
  7. django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
  8. django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
  9. django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
  10. django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
  11. django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
  12. django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
  13. django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
  14. django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
  15. django_cfg/apps/business/support/serializers.py +3 -2
  16. django_cfg/apps/integrations/centrifugo/apps.py +2 -1
  17. django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
  18. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
  19. django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
  20. django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
  21. django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
  22. django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
  23. django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
  24. django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
  25. django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
  26. django_cfg/apps/integrations/centrifugo/urls.py +8 -0
  27. django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
  28. django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
  29. django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
  30. django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
  31. django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
  32. django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
  33. django_cfg/apps/integrations/grpc/admin/config.py +113 -9
  34. django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
  35. django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
  36. django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
  37. django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
  38. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
  39. django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
  40. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
  41. django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
  42. django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
  43. django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
  44. django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
  45. django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
  46. django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
  47. django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
  48. django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
  49. django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
  50. django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
  51. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
  52. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
  53. django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
  54. django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
  55. django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
  56. django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
  57. django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
  58. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
  59. django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
  60. django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
  61. django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
  62. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
  63. django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
  64. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
  65. django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
  66. django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
  67. django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
  68. django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
  69. django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
  70. django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
  71. django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
  72. django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
  73. django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
  74. django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
  75. django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
  76. django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
  77. django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
  78. django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
  79. django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
  80. django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
  81. django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
  82. django_cfg/apps/integrations/grpc/urls.py +8 -0
  83. django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
  84. django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
  85. django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
  86. django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
  87. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
  88. django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
  89. django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
  90. django_cfg/apps/integrations/grpc/views/charts.py +21 -14
  91. django_cfg/apps/integrations/grpc/views/config.py +8 -6
  92. django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
  93. django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
  94. django_cfg/apps/integrations/grpc/views/services.py +30 -21
  95. django_cfg/apps/integrations/grpc/views/testing.py +45 -43
  96. django_cfg/apps/integrations/rq/views/jobs.py +19 -9
  97. django_cfg/apps/integrations/rq/views/schedule.py +7 -3
  98. django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
  99. django_cfg/apps/system/dashboard/serializers/config.py +95 -9
  100. django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
  101. django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
  102. django_cfg/apps/system/frontend/views.py +87 -6
  103. django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
  104. django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
  105. django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
  106. django_cfg/config.py +33 -0
  107. django_cfg/core/builders/security_builder.py +1 -0
  108. django_cfg/core/generation/integration_generators/api.py +2 -0
  109. django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
  110. django_cfg/management/commands/check_endpoints.py +2 -2
  111. django_cfg/management/commands/check_settings.py +3 -10
  112. django_cfg/management/commands/clear_constance.py +3 -10
  113. django_cfg/management/commands/create_token.py +4 -11
  114. django_cfg/management/commands/list_urls.py +4 -10
  115. django_cfg/management/commands/migrate_all.py +18 -12
  116. django_cfg/management/commands/migrator.py +4 -11
  117. django_cfg/management/commands/script.py +4 -10
  118. django_cfg/management/commands/show_config.py +8 -16
  119. django_cfg/management/commands/show_urls.py +5 -11
  120. django_cfg/management/commands/superuser.py +4 -11
  121. django_cfg/management/commands/tree.py +5 -10
  122. django_cfg/management/utils/README.md +402 -0
  123. django_cfg/management/utils/__init__.py +29 -0
  124. django_cfg/management/utils/mixins.py +176 -0
  125. django_cfg/middleware/pagination.py +53 -54
  126. django_cfg/models/api/grpc/__init__.py +15 -21
  127. django_cfg/models/api/grpc/config.py +155 -73
  128. django_cfg/models/ngrok/config.py +7 -6
  129. django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
  130. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
  131. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
  132. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
  133. django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
  134. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
  135. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
  136. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
  137. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
  138. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
  139. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
  140. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
  141. django_cfg/modules/django_client/core/ir/schema.py +15 -1
  142. django_cfg/modules/django_client/core/parser/base.py +126 -30
  143. django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
  144. django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
  145. django_cfg/modules/django_email/management/commands/test_email.py +4 -10
  146. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
  147. django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
  148. django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
  149. django_cfg/modules/django_unfold/navigation.py +6 -18
  150. django_cfg/pyproject.toml +1 -1
  151. django_cfg/registry/modules.py +1 -4
  152. django_cfg/requirements.txt +52 -0
  153. django_cfg/static/frontend/admin.zip +0 -0
  154. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
  155. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
  156. django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
  157. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
  158. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
  159. {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 field
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
- "status_badge",
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
- __all__ = ["grpcrequestlog_config", "grpcserverstatus_config"]
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"]