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
|
@@ -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.
|
|
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="
|
|
135
|
+
name="page",
|
|
133
136
|
type=OpenApiTypes.INT,
|
|
134
137
|
location=OpenApiParameter.QUERY,
|
|
135
|
-
description="
|
|
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:
|
|
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")
|
|
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
|
-
#
|
|
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
|
-
|
|
186
|
-
total = queryset.count()
|
|
184
|
+
queryset = queryset.order_by("-created_at")
|
|
187
185
|
|
|
188
|
-
#
|
|
186
|
+
# Convert queryset to list of dicts for serialization
|
|
189
187
|
publishes_list = list(
|
|
190
|
-
queryset.
|
|
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
|
-
#
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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}")
|