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
@@ -0,0 +1,371 @@
1
+ """
2
+ Centrifugo Publishing Service.
3
+
4
+ Unified high-level API for publishing events to Centrifugo.
5
+ Abstracts away CentrifugoClient details and provides domain-specific methods.
6
+
7
+ Usage:
8
+ >>> from django_cfg.apps.integrations.centrifugo.services import CentrifugoPublisher
9
+ >>>
10
+ >>> publisher = CentrifugoPublisher()
11
+ >>>
12
+ >>> # Publish gRPC event
13
+ >>> await publisher.publish_grpc_event(
14
+ ... channel="grpc#bot#123#status",
15
+ ... method="/bot.BotService/Start",
16
+ ... status="OK",
17
+ ... duration_ms=150
18
+ ... )
19
+ >>>
20
+ >>> # Publish demo event
21
+ >>> await publisher.publish_demo_event(
22
+ ... channel="grpc#demo#test",
23
+ ... metadata={"test": True}
24
+ ... )
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from datetime import datetime, timezone as tz
30
+ from typing import Any, Dict, Optional
31
+
32
+ from django_cfg.modules.django_logging import get_logger
33
+
34
+ from ..services.client import (
35
+ CentrifugoClient,
36
+ DirectCentrifugoClient,
37
+ PublishResponse,
38
+ get_centrifugo_client,
39
+ get_direct_centrifugo_client,
40
+ )
41
+
42
+ logger = get_logger("centrifugo.publisher")
43
+
44
+
45
+ class CentrifugoPublisher:
46
+ """
47
+ High-level publishing service for Centrifugo events.
48
+
49
+ Provides domain-specific methods that abstract away low-level client details.
50
+ All methods are async and handle errors gracefully.
51
+
52
+ Features:
53
+ - Unified API for all Centrifugo publishing
54
+ - Automatic timestamp injection
55
+ - Type-safe event metadata
56
+ - Error handling and logging
57
+ - Easy to mock for testing
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ client: Optional[CentrifugoClient | DirectCentrifugoClient] = None,
63
+ use_direct: bool = True,
64
+ ):
65
+ """
66
+ Initialize publisher.
67
+
68
+ Args:
69
+ client: Optional client instance (CentrifugoClient or DirectCentrifugoClient)
70
+ use_direct: Use DirectCentrifugoClient (bypass wrapper, default=True)
71
+ """
72
+ if client:
73
+ self._client = client
74
+ logger.debug("CentrifugoPublisher initialized with custom client")
75
+ elif use_direct:
76
+ # Use direct client (no wrapper, no DB logging)
77
+ self._client = get_direct_centrifugo_client()
78
+ logger.debug("CentrifugoPublisher initialized with DirectCentrifugoClient")
79
+ else:
80
+ # Use wrapper client (with auth & DB logging)
81
+ self._client = get_centrifugo_client()
82
+ logger.debug("CentrifugoPublisher initialized with CentrifugoClient (wrapper)")
83
+
84
+ @property
85
+ def client(self) -> CentrifugoClient | DirectCentrifugoClient:
86
+ """Get underlying client instance."""
87
+ return self._client
88
+
89
+ async def publish_grpc_event(
90
+ self,
91
+ channel: str,
92
+ method: str,
93
+ status: str = "OK",
94
+ duration_ms: float = 0.0,
95
+ peer: Optional[str] = None,
96
+ metadata: Optional[Dict[str, Any]] = None,
97
+ **extra: Any,
98
+ ) -> PublishResponse:
99
+ """
100
+ Publish gRPC event (interceptor-style metadata).
101
+
102
+ Args:
103
+ channel: Centrifugo channel (e.g., "grpc#bot#123#status")
104
+ method: Full gRPC method name (e.g., "/bot.BotService/Start")
105
+ status: RPC status code (default: "OK")
106
+ duration_ms: RPC duration in milliseconds
107
+ peer: Client peer address
108
+ metadata: Additional metadata dict
109
+ **extra: Additional fields
110
+
111
+ Returns:
112
+ PublishResponse with result
113
+
114
+ Example:
115
+ >>> await publisher.publish_grpc_event(
116
+ ... channel="grpc#bot#123#status",
117
+ ... method="/bot.BotService/Start",
118
+ ... status="OK",
119
+ ... duration_ms=150,
120
+ ... peer="127.0.0.1:50051"
121
+ ... )
122
+ """
123
+ # Parse method name
124
+ service_name = None
125
+ method_name = None
126
+ if method.startswith("/") and "/" in method[1:]:
127
+ parts = method[1:].split("/")
128
+ service_name = parts[0]
129
+ method_name = parts[1]
130
+
131
+ # Build event data
132
+ event_data = {
133
+ "event_type": "grpc_event",
134
+ "method": method,
135
+ "status": status,
136
+ "timestamp": datetime.now(tz.utc).isoformat(),
137
+ }
138
+
139
+ if service_name:
140
+ event_data["service"] = service_name
141
+ if method_name:
142
+ event_data["method_name"] = method_name
143
+ if duration_ms:
144
+ event_data["duration_ms"] = duration_ms
145
+ if peer:
146
+ event_data["peer"] = peer
147
+ if metadata:
148
+ event_data.update(metadata)
149
+ if extra:
150
+ event_data.update(extra)
151
+
152
+ logger.debug(f"Publishing gRPC event: {channel} ({method})")
153
+
154
+ # DirectCentrifugoClient uses simpler API
155
+ if isinstance(self._client, DirectCentrifugoClient):
156
+ return await self._client.publish(channel=channel, data=event_data)
157
+ else:
158
+ return await self._client.publish(channel=channel, data=event_data)
159
+
160
+ async def publish_demo_event(
161
+ self,
162
+ channel: str,
163
+ metadata: Optional[Dict[str, Any]] = None,
164
+ **extra: Any,
165
+ ) -> PublishResponse:
166
+ """
167
+ Publish demo/test event.
168
+
169
+ Args:
170
+ channel: Centrifugo channel
171
+ metadata: Event metadata
172
+ **extra: Additional fields
173
+
174
+ Returns:
175
+ PublishResponse with result
176
+
177
+ Example:
178
+ >>> await publisher.publish_demo_event(
179
+ ... channel="grpc#demo#test",
180
+ ... metadata={"test": True, "source": "demo.py"}
181
+ ... )
182
+ """
183
+ event_data = {
184
+ "event_type": "demo_event",
185
+ "timestamp": datetime.now(tz.utc).isoformat(),
186
+ "test_mode": True,
187
+ }
188
+
189
+ if metadata:
190
+ event_data.update(metadata)
191
+ if extra:
192
+ event_data.update(extra)
193
+
194
+ logger.debug(f"Publishing demo event: {channel}")
195
+
196
+ if isinstance(self._client, DirectCentrifugoClient):
197
+ return await self._client.publish(channel=channel, data=event_data)
198
+ else:
199
+ return await self._client.publish(channel=channel, data=event_data)
200
+
201
+ async def publish_notification(
202
+ self,
203
+ channel: str,
204
+ title: str,
205
+ message: str,
206
+ level: str = "info",
207
+ user: Optional[Any] = None,
208
+ metadata: Optional[Dict[str, Any]] = None,
209
+ **extra: Any,
210
+ ) -> PublishResponse:
211
+ """
212
+ Publish user notification.
213
+
214
+ Args:
215
+ channel: Centrifugo channel (e.g., "notifications#user#123")
216
+ title: Notification title
217
+ message: Notification message
218
+ level: Notification level (info, warning, error, success)
219
+ user: Django User instance
220
+ metadata: Additional metadata
221
+ **extra: Additional fields
222
+
223
+ Returns:
224
+ PublishResponse with result
225
+
226
+ Example:
227
+ >>> await publisher.publish_notification(
228
+ ... channel="notifications#user#123",
229
+ ... title="Bot Started",
230
+ ... message="Your bot has started successfully",
231
+ ... level="success"
232
+ ... )
233
+ """
234
+ event_data = {
235
+ "event_type": "notification",
236
+ "title": title,
237
+ "message": message,
238
+ "level": level,
239
+ "timestamp": datetime.now(tz.utc).isoformat(),
240
+ }
241
+
242
+ if metadata:
243
+ event_data.update(metadata)
244
+ if extra:
245
+ event_data.update(extra)
246
+
247
+ logger.debug(f"Publishing notification: {channel} ({title})")
248
+
249
+ return await self._client.publish(channel=channel, data=event_data, user=user)
250
+
251
+ async def publish_status_change(
252
+ self,
253
+ channel: str,
254
+ old_status: str,
255
+ new_status: str,
256
+ reason: Optional[str] = None,
257
+ metadata: Optional[Dict[str, Any]] = None,
258
+ **extra: Any,
259
+ ) -> PublishResponse:
260
+ """
261
+ Publish status change event.
262
+
263
+ Args:
264
+ channel: Centrifugo channel
265
+ old_status: Previous status
266
+ new_status: New status
267
+ reason: Reason for status change
268
+ metadata: Additional metadata
269
+ **extra: Additional fields
270
+
271
+ Returns:
272
+ PublishResponse with result
273
+
274
+ Example:
275
+ >>> await publisher.publish_status_change(
276
+ ... channel="bot#123#status",
277
+ ... old_status="STOPPED",
278
+ ... new_status="RUNNING",
279
+ ... reason="User requested start"
280
+ ... )
281
+ """
282
+ event_data = {
283
+ "event_type": "status_change",
284
+ "old_status": old_status,
285
+ "new_status": new_status,
286
+ "timestamp": datetime.now(tz.utc).isoformat(),
287
+ }
288
+
289
+ if reason:
290
+ event_data["reason"] = reason
291
+ if metadata:
292
+ event_data.update(metadata)
293
+ if extra:
294
+ event_data.update(extra)
295
+
296
+ logger.debug(f"Publishing status change: {channel} ({old_status} → {new_status})")
297
+
298
+ return await self._client.publish(channel=channel, data=event_data)
299
+
300
+ async def publish_custom(
301
+ self,
302
+ channel: str,
303
+ event_type: str,
304
+ data: Dict[str, Any],
305
+ user: Optional[Any] = None,
306
+ ) -> PublishResponse:
307
+ """
308
+ Publish custom event with arbitrary data.
309
+
310
+ Args:
311
+ channel: Centrifugo channel
312
+ event_type: Custom event type
313
+ data: Event data dict
314
+ user: Django User instance
315
+
316
+ Returns:
317
+ PublishResponse with result
318
+
319
+ Example:
320
+ >>> await publisher.publish_custom(
321
+ ... channel="custom#events",
322
+ ... event_type="custom_event",
323
+ ... data={"foo": "bar", "count": 42}
324
+ ... )
325
+ """
326
+ event_data = {
327
+ "event_type": event_type,
328
+ "timestamp": datetime.now(tz.utc).isoformat(),
329
+ **data,
330
+ }
331
+
332
+ logger.debug(f"Publishing custom event: {channel} ({event_type})")
333
+
334
+ return await self._client.publish(channel=channel, data=event_data, user=user)
335
+
336
+
337
+ # Singleton instance
338
+ _publisher_instance: Optional[CentrifugoPublisher] = None
339
+
340
+
341
+ def get_centrifugo_publisher(client: Optional[CentrifugoClient] = None) -> CentrifugoPublisher:
342
+ """
343
+ Get singleton CentrifugoPublisher instance.
344
+
345
+ Args:
346
+ client: Optional CentrifugoClient (creates new publisher if provided)
347
+
348
+ Returns:
349
+ CentrifugoPublisher instance
350
+
351
+ Example:
352
+ >>> from django_cfg.apps.integrations.centrifugo.services import get_centrifugo_publisher
353
+ >>> publisher = get_centrifugo_publisher()
354
+ >>> await publisher.publish_demo_event(channel="test", metadata={"foo": "bar"})
355
+ """
356
+ global _publisher_instance
357
+
358
+ if client is not None:
359
+ # Create new instance with custom client
360
+ return CentrifugoPublisher(client=client)
361
+
362
+ if _publisher_instance is None:
363
+ _publisher_instance = CentrifugoPublisher()
364
+
365
+ return _publisher_instance
366
+
367
+
368
+ __all__ = [
369
+ "CentrifugoPublisher",
370
+ "get_centrifugo_publisher",
371
+ ]
@@ -0,0 +1,122 @@
1
+ """
2
+ Centrifugo Token Generator Service.
3
+
4
+ Provides utilities for generating Centrifugo JWT tokens with user permissions.
5
+ """
6
+
7
+ import time
8
+ import jwt
9
+ from datetime import datetime, timezone
10
+ from typing import List, Dict, Any, Optional
11
+
12
+ from .config_helper import get_centrifugo_config
13
+
14
+
15
+ def get_user_channels(user) -> List[str]:
16
+ """
17
+ Get list of Centrifugo channels user is allowed to subscribe to.
18
+
19
+ Args:
20
+ user: Django user instance
21
+
22
+ Returns:
23
+ List of channel names user can access
24
+
25
+ Channel naming convention:
26
+ - user#{user_id} - Personal channel for RPC responses
27
+ - notifications#user#{user_id} - Personal notifications
28
+ - centrifugo#dashboard - Admin dashboard events
29
+ - admin#notifications - Admin notifications
30
+ - grpc#* - All gRPC bot events (admin only)
31
+ - broadcast - Global broadcast channel
32
+ """
33
+ channels = []
34
+
35
+ # Personal channel for RPC responses
36
+ channels.append(f"user#{user.id}")
37
+
38
+ # Notifications channel
39
+ channels.append(f"notifications#user#{user.id}")
40
+
41
+ # Admin channels
42
+ if user.is_staff or user.is_superuser:
43
+ channels.append("centrifugo#dashboard")
44
+ channels.append("admin#notifications")
45
+ # Allow admins to see all gRPC bot events
46
+ channels.append("grpc#*")
47
+
48
+ # Broadcast channel for all users
49
+ channels.append("broadcast")
50
+
51
+ return channels
52
+
53
+
54
+ def generate_centrifugo_token(
55
+ user,
56
+ exp_seconds: int = 3600,
57
+ additional_channels: Optional[List[str]] = None
58
+ ) -> Dict[str, Any]:
59
+ """
60
+ Generate Centrifugo JWT token with user's allowed channels.
61
+
62
+ Args:
63
+ user: Django user instance
64
+ exp_seconds: Token expiration time in seconds (default: 1 hour)
65
+ additional_channels: Optional additional channels to include
66
+
67
+ Returns:
68
+ Dictionary with:
69
+ - token: JWT token string
70
+ - centrifugo_url: Centrifugo WebSocket URL
71
+ - expires_at: Token expiration datetime
72
+ - channels: List of allowed channels
73
+
74
+ Raises:
75
+ ValueError: If Centrifugo is not configured or disabled
76
+ """
77
+ config = get_centrifugo_config()
78
+ if not config or not config.enabled:
79
+ raise ValueError("Centrifugo not configured or disabled")
80
+
81
+ # Get user's allowed channels
82
+ channels = get_user_channels(user)
83
+
84
+ # Add additional channels if provided
85
+ if additional_channels:
86
+ channels.extend(additional_channels)
87
+ # Remove duplicates while preserving order
88
+ channels = list(dict.fromkeys(channels))
89
+
90
+ # Generate JWT token
91
+ now = int(time.time())
92
+ exp = now + exp_seconds
93
+
94
+ payload = {
95
+ "sub": str(user.id), # User ID
96
+ "exp": exp, # Expiration time
97
+ "iat": now, # Issued at
98
+ "channels": channels, # Allowed channels
99
+ }
100
+
101
+ # Sign token with HMAC secret
102
+ token = jwt.encode(
103
+ payload,
104
+ config.centrifugo_token_hmac_secret,
105
+ algorithm="HS256"
106
+ )
107
+
108
+ # Use timezone-aware datetime for proper ISO 8601 format
109
+ expires_at = datetime.fromtimestamp(exp, tz=timezone.utc)
110
+
111
+ return {
112
+ "token": token,
113
+ "centrifugo_url": config.centrifugo_url,
114
+ "expires_at": expires_at,
115
+ "channels": channels,
116
+ }
117
+
118
+
119
+ __all__ = [
120
+ "get_user_channels",
121
+ "generate_centrifugo_token",
122
+ ]
@@ -10,6 +10,8 @@ from rest_framework import routers
10
10
  from .views.admin_api import CentrifugoAdminAPIViewSet
11
11
  from .views.monitoring import CentrifugoMonitorViewSet
12
12
  from .views.testing_api import CentrifugoTestingAPIViewSet
13
+ from .views.token_api import CentrifugoTokenViewSet
14
+ from .views.wrapper import PublishWrapperView
13
15
 
14
16
  app_name = 'django_cfg_centrifugo'
15
17
 
@@ -25,7 +27,13 @@ router.register(r'server', CentrifugoAdminAPIViewSet, basename='server')
25
27
  # Testing API endpoints (live testing from dashboard)
26
28
  router.register(r'testing', CentrifugoTestingAPIViewSet, basename='testing')
27
29
 
30
+ # Token API endpoints (JWT token generation for client connections)
31
+ router.register(r'auth', CentrifugoTokenViewSet, basename='auth')
32
+
28
33
  urlpatterns = [
34
+ # Wrapper API endpoint (for CentrifugoClient)
35
+ path('api/publish', PublishWrapperView.as_view(), name='wrapper_publish'),
36
+
29
37
  # Include router URLs
30
38
  path('', include(router.urls)),
31
39
  ]
@@ -5,9 +5,11 @@ Views for Centrifugo module.
5
5
  from .admin_api import CentrifugoAdminAPIViewSet
6
6
  from .monitoring import CentrifugoMonitorViewSet
7
7
  from .testing_api import CentrifugoTestingAPIViewSet
8
+ from .wrapper import PublishWrapperView
8
9
 
9
10
  __all__ = [
10
11
  'CentrifugoMonitorViewSet',
11
12
  'CentrifugoAdminAPIViewSet',
12
13
  'CentrifugoTestingAPIViewSet',
14
+ 'PublishWrapperView',
13
15
  ]
@@ -23,6 +23,7 @@ from ..serializers import (
23
23
  ChannelListSerializer,
24
24
  ChannelStatsSerializer,
25
25
  HealthCheckSerializer,
26
+ PublishSerializer,
26
27
  RecentPublishesSerializer,
27
28
  )
28
29
  from ..services import get_centrifugo_config
@@ -30,18 +31,20 @@ from ..services import get_centrifugo_config
30
31
  logger = get_logger("centrifugo.monitoring")
31
32
 
32
33
 
33
- class CentrifugoMonitorViewSet(AdminAPIMixin, viewsets.ViewSet):
34
+ class CentrifugoMonitorViewSet(AdminAPIMixin, viewsets.GenericViewSet):
34
35
  """
35
36
  ViewSet for Centrifugo monitoring and statistics.
36
37
 
37
38
  Provides comprehensive monitoring data for Centrifugo publishes including:
38
39
  - Health checks
39
40
  - Overview statistics
40
- - Recent publishes
41
+ - Recent publishes (with DRF pagination)
41
42
  - Channel-level statistics
42
43
  Requires admin authentication (JWT, Session, or Basic Auth).
43
44
  """
44
45
 
46
+ serializer_class = PublishSerializer
47
+
45
48
  @extend_schema(
46
49
  tags=["Centrifugo Monitoring"],
47
50
  summary="Get Centrifugo health status",
@@ -126,13 +129,20 @@ class CentrifugoMonitorViewSet(AdminAPIMixin, viewsets.ViewSet):
126
129
  @extend_schema(
127
130
  tags=["Centrifugo Monitoring"],
128
131
  summary="Get recent publishes",
129
- description="Returns a list of recent Centrifugo publishes with their details.",
132
+ description="Returns a paginated list of recent Centrifugo publishes with their details. Uses standard DRF pagination.",
130
133
  parameters=[
131
134
  OpenApiParameter(
132
- name="count",
135
+ name="page",
133
136
  type=OpenApiTypes.INT,
134
137
  location=OpenApiParameter.QUERY,
135
- description="Number of publishes to return (default: 50, max: 200)",
138
+ description="Page number",
139
+ required=False,
140
+ ),
141
+ OpenApiParameter(
142
+ name="page_size",
143
+ type=OpenApiTypes.INT,
144
+ location=OpenApiParameter.QUERY,
145
+ description="Items per page (default: 10, max: 100)",
136
146
  required=False,
137
147
  ),
138
148
  OpenApiParameter(
@@ -149,16 +159,9 @@ class CentrifugoMonitorViewSet(AdminAPIMixin, viewsets.ViewSet):
149
159
  description="Filter by status (success, failed, timeout, pending, partial)",
150
160
  required=False,
151
161
  ),
152
- OpenApiParameter(
153
- name="offset",
154
- type=OpenApiTypes.INT,
155
- location=OpenApiParameter.QUERY,
156
- description="Offset for pagination (default: 0)",
157
- required=False,
158
- ),
159
162
  ],
160
163
  responses={
161
- 200: RecentPublishesSerializer,
164
+ 200: PublishSerializer(many=True),
162
165
  400: {"description": "Invalid parameters"},
163
166
  },
164
167
  )
@@ -166,28 +169,23 @@ class CentrifugoMonitorViewSet(AdminAPIMixin, viewsets.ViewSet):
166
169
  def publishes(self, request):
167
170
  """Get recent Centrifugo publishes."""
168
171
  try:
169
- count = int(request.GET.get("count", 50))
170
- count = min(count, 200) # Max 200
171
-
172
172
  channel = request.GET.get("channel")
173
- status_filter = request.GET.get("status") # NEW: status filter
174
- offset = int(request.GET.get("offset", 0)) # NEW: offset for pagination
173
+ status_filter = request.GET.get("status")
175
174
 
176
175
  queryset = CentrifugoLog.objects.all()
177
176
 
178
177
  if channel:
179
178
  queryset = queryset.filter(channel=channel)
180
179
 
181
- # NEW: Filter by status
180
+ # Filter by status
182
181
  if status_filter and status_filter in ["success", "failed", "timeout", "pending", "partial"]:
183
182
  queryset = queryset.filter(status=status_filter)
184
183
 
185
- # Get total count before slicing
186
- total = queryset.count()
184
+ queryset = queryset.order_by("-created_at")
187
185
 
188
- # NEW: Apply offset and limit
186
+ # Convert queryset to list of dicts for serialization
189
187
  publishes_list = list(
190
- queryset.order_by("-created_at")[offset:offset + count].values(
188
+ queryset.values(
191
189
  "message_id",
192
190
  "channel",
193
191
  "status",
@@ -202,23 +200,10 @@ class CentrifugoMonitorViewSet(AdminAPIMixin, viewsets.ViewSet):
202
200
  )
203
201
  )
204
202
 
205
- # Convert datetime to ISO format
206
- for pub in publishes_list:
207
- if pub["created_at"]:
208
- pub["created_at"] = pub["created_at"].isoformat()
209
- if pub["completed_at"]:
210
- pub["completed_at"] = pub["completed_at"].isoformat()
211
-
212
- response_data = {
213
- "publishes": publishes_list,
214
- "count": len(publishes_list),
215
- "total_available": total,
216
- "offset": offset, # NEW: for pagination
217
- "has_more": (offset + count) < total, # NEW: pagination helper
218
- }
219
-
220
- serializer = RecentPublishesSerializer(**response_data)
221
- return Response(serializer.model_dump())
203
+ # Use DRF pagination
204
+ page = self.paginate_queryset(publishes_list)
205
+ serializer = PublishSerializer(page, many=True)
206
+ return self.get_paginated_response(serializer.data)
222
207
 
223
208
  except ValueError as e:
224
209
  logger.warning(f"Recent publishes validation error: {e}")