django-cfg 1.5.14__py3-none-any.whl → 1.5.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (118) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/business/accounts/serializers/profile.py +42 -0
  3. django_cfg/apps/business/support/serializers.py +3 -2
  4. django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
  5. django_cfg/apps/integrations/centrifugo/apps.py +2 -1
  6. django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
  7. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +2 -2
  8. django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
  9. django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
  10. django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
  11. django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
  12. django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
  13. django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
  14. django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
  15. django_cfg/apps/integrations/centrifugo/urls.py +8 -0
  16. django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
  17. django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
  18. django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -116
  19. django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
  20. django_cfg/apps/integrations/centrifugo/views/wrapper.py +259 -0
  21. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
  22. django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
  23. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +56 -1
  24. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +315 -26
  25. django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
  26. django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
  27. django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
  28. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
  29. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
  30. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
  31. django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
  32. django_cfg/apps/integrations/grpc/services/centrifugo/__init__.py +29 -0
  33. django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
  34. django_cfg/apps/integrations/grpc/services/centrifugo/config.py +167 -0
  35. django_cfg/apps/integrations/grpc/services/centrifugo/demo.py +626 -0
  36. django_cfg/apps/integrations/grpc/services/centrifugo/test_publish.py +229 -0
  37. django_cfg/apps/integrations/grpc/services/centrifugo/transformers.py +89 -0
  38. django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
  39. django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
  40. django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
  41. django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
  42. django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
  43. django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
  44. django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
  45. django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
  46. django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
  47. django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
  48. django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
  49. django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
  50. django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
  51. django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +67 -54
  52. django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
  53. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +3 -1
  54. django_cfg/apps/integrations/grpc/services/interceptors/centrifugo.py +541 -0
  55. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
  56. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
  57. django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
  58. django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
  59. django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
  60. django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
  61. django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
  62. django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
  63. django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
  64. django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
  65. django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
  66. django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
  67. django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
  68. django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
  69. django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
  70. django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
  71. django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
  72. django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
  73. django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
  74. django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
  75. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +261 -13
  76. django_cfg/apps/integrations/grpc/views/charts.py +1 -1
  77. django_cfg/apps/integrations/grpc/views/config.py +1 -1
  78. django_cfg/apps/system/dashboard/serializers/config.py +95 -9
  79. django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
  80. django_cfg/apps/system/frontend/views.py +87 -6
  81. django_cfg/core/base/config_model.py +11 -0
  82. django_cfg/core/builders/middleware_builder.py +5 -0
  83. django_cfg/core/builders/security_builder.py +1 -0
  84. django_cfg/core/generation/integration_generators/api.py +2 -0
  85. django_cfg/management/commands/pool_status.py +153 -0
  86. django_cfg/middleware/pool_cleanup.py +261 -0
  87. django_cfg/models/api/grpc/config.py +2 -2
  88. django_cfg/models/infrastructure/database/config.py +16 -0
  89. django_cfg/models/infrastructure/database/converters.py +2 -0
  90. django_cfg/modules/django_admin/utils/html/composition.py +57 -13
  91. django_cfg/modules/django_admin/utils/html_builder.py +1 -0
  92. django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
  93. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
  94. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
  95. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
  96. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
  97. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
  98. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
  99. django_cfg/modules/django_client/core/groups/manager.py +25 -18
  100. django_cfg/modules/django_client/core/ir/schema.py +15 -1
  101. django_cfg/modules/django_client/core/parser/base.py +12 -0
  102. django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
  103. django_cfg/modules/django_logging/django_logger.py +58 -19
  104. django_cfg/pyproject.toml +3 -3
  105. django_cfg/static/frontend/admin.zip +0 -0
  106. django_cfg/templates/admin/index.html +0 -39
  107. django_cfg/utils/pool_monitor.py +320 -0
  108. django_cfg/utils/smart_defaults.py +233 -7
  109. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
  110. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/RECORD +118 -74
  111. /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
  112. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
  113. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
  114. /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
  115. /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
  116. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
  117. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
  118. {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.5.14"
35
+ __version__ = "1.5.29"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Setup warnings debug early (checks env var only at this point)
@@ -4,6 +4,24 @@ from rest_framework import serializers
4
4
  from ..models import CustomUser, RegistrationSource, UserRegistrationSource
5
5
 
6
6
 
7
+ class CentrifugoTokenSerializer(serializers.Serializer):
8
+ """Nested serializer for Centrifugo WebSocket connection token."""
9
+
10
+ token = serializers.CharField(
11
+ help_text="JWT token for Centrifugo WebSocket connection"
12
+ )
13
+ centrifugo_url = serializers.URLField(
14
+ help_text="Centrifugo WebSocket URL"
15
+ )
16
+ expires_at = serializers.DateTimeField(
17
+ help_text="Token expiration time (ISO 8601)"
18
+ )
19
+ channels = serializers.ListField(
20
+ child=serializers.CharField(),
21
+ help_text="List of allowed channels for this user"
22
+ )
23
+
24
+
7
25
  class UserSerializer(serializers.ModelSerializer):
8
26
  """Serializer for user details."""
9
27
 
@@ -11,6 +29,7 @@ class UserSerializer(serializers.ModelSerializer):
11
29
  initials = serializers.ReadOnlyField()
12
30
  display_username = serializers.ReadOnlyField()
13
31
  avatar = serializers.SerializerMethodField()
32
+ centrifugo = serializers.SerializerMethodField()
14
33
 
15
34
  class Meta:
16
35
  model = CustomUser
@@ -31,6 +50,7 @@ class UserSerializer(serializers.ModelSerializer):
31
50
  "date_joined",
32
51
  "last_login",
33
52
  "unanswered_messages_count",
53
+ "centrifugo",
34
54
  ]
35
55
  read_only_fields = [
36
56
  "id",
@@ -52,6 +72,28 @@ class UserSerializer(serializers.ModelSerializer):
52
72
  return obj.avatar.url
53
73
  return None
54
74
 
75
+ @extend_schema_field(CentrifugoTokenSerializer(allow_null=True))
76
+ def get_centrifugo(self, obj):
77
+ """
78
+ Generate Centrifugo WebSocket connection token if enabled.
79
+
80
+ Returns None if Centrifugo is disabled in config.
81
+ """
82
+ try:
83
+ # Import here to avoid circular imports
84
+ from django_cfg.apps.integrations.centrifugo.services import generate_centrifugo_token
85
+
86
+ # Generate token with user's channels
87
+ token_data = generate_centrifugo_token(obj)
88
+ return token_data
89
+
90
+ except ValueError:
91
+ # Centrifugo not configured or disabled
92
+ return None
93
+ except Exception:
94
+ # If token generation fails, return None (don't break profile response)
95
+ return None
96
+
55
97
 
56
98
 
57
99
  class UserProfileUpdateSerializer(serializers.ModelSerializer):
@@ -8,13 +8,14 @@ from .models import Message, Ticket
8
8
  User = get_user_model()
9
9
 
10
10
  class SenderSerializer(serializers.ModelSerializer):
11
- avatar = serializers.SerializerMethodField()
11
+ avatar = serializers.SerializerMethodField(allow_null=True)
12
12
  initials = serializers.ReadOnlyField()
13
13
 
14
14
  class Meta:
15
15
  model = User
16
16
  fields = ['id', 'display_username', 'email', 'avatar', 'initials', 'is_staff', 'is_superuser']
17
- read_only_fields = fields
17
+ # Don't include avatar in read_only_fields to make it optional in OpenAPI schema
18
+ read_only_fields = ['id', 'display_username', 'email', 'initials', 'is_staff', 'is_superuser']
18
19
 
19
20
  def get_avatar(self, obj) -> Optional[str]:
20
21
  if obj.avatar:
@@ -23,6 +23,7 @@ Example:
23
23
  """
24
24
 
25
25
  from .services.client.client import CentrifugoClient, get_centrifugo_client, PublishResponse
26
+ from .services.client.direct_client import DirectCentrifugoClient
26
27
  from .services.client.config import DjangoCfgCentrifugoConfig
27
28
  from .services.client.exceptions import (
28
29
  CentrifugoBaseException,
@@ -42,6 +43,7 @@ __all__ = [
42
43
  "DjangoCfgCentrifugoConfig",
43
44
  # Client
44
45
  "CentrifugoClient",
46
+ "DirectCentrifugoClient",
45
47
  "get_centrifugo_client",
46
48
  "PublishResponse",
47
49
  # Logging
@@ -32,6 +32,7 @@ class CentrifugoConfig(AppConfig):
32
32
  Initialize app when Django starts.
33
33
 
34
34
  Validates that all required Centrifugo dependencies are installed.
35
+ Registers signal handlers for JWT token customization.
35
36
  """
36
37
  from django_cfg.modules.django_logging import get_logger
37
38
 
@@ -40,7 +41,7 @@ class CentrifugoConfig(AppConfig):
40
41
  # Check dependencies if needed (only when using Centrifugo features)
41
42
  self._check_dependencies_if_needed()
42
43
 
43
- logger.info("Centrifugo app initialized")
44
+ logger.info("Centrifugo app initialized (middleware will inject JWT tokens)")
44
45
 
45
46
  def _check_dependencies_if_needed(self):
46
47
  """
@@ -10,6 +10,7 @@ import { Centrifuge } from 'centrifuge';
10
10
  export class CentrifugoRPCClient {
11
11
  private centrifuge: Centrifuge;
12
12
  private subscription: any;
13
+ private channelSubscriptions: Map<string, any> = new Map();
13
14
  private pendingRequests: Map<string, { resolve: Function; reject: Function }> = new Map();
14
15
  private readonly replyChannel: string;
15
16
  private readonly timeout: number;
@@ -27,12 +28,7 @@ export class CentrifugoRPCClient {
27
28
  token,
28
29
  });
29
30
 
30
- this.centrifuge.on('connected', (ctx) => {
31
- console.log('✅ Connected to Centrifugo');
32
- });
33
-
34
31
  this.centrifuge.on('disconnected', (ctx) => {
35
- console.warn('Disconnected:', ctx);
36
32
  // Reject all pending requests
37
33
  this.pendingRequests.forEach(({ reject }) => {
38
34
  reject(new Error('Disconnected from Centrifugo'));
@@ -43,6 +39,27 @@ export class CentrifugoRPCClient {
43
39
 
44
40
  async connect(): Promise<void> {
45
41
  return new Promise((resolve, reject) => {
42
+ let resolved = false;
43
+
44
+ // Listen to Centrifuge connection events
45
+ const onConnected = () => {
46
+ if (!resolved) {
47
+ resolved = true;
48
+ resolve();
49
+ }
50
+ };
51
+
52
+ const onError = (ctx: any) => {
53
+ if (!resolved) {
54
+ resolved = true;
55
+ reject(new Error(ctx.message || 'Connection error'));
56
+ }
57
+ };
58
+
59
+ this.centrifuge.on('connected', onConnected);
60
+ this.centrifuge.on('error', onError);
61
+
62
+ // Start connection
46
63
  this.centrifuge.connect();
47
64
 
48
65
  // Subscribe to reply channel
@@ -53,11 +70,17 @@ export class CentrifugoRPCClient {
53
70
  });
54
71
 
55
72
  this.subscription.on('subscribed', () => {
56
- resolve();
73
+ // Subscription successful (optional, we already resolved on 'connected')
57
74
  });
58
75
 
59
76
  this.subscription.on('error', (ctx: any) => {
60
- reject(new Error(ctx.error?.message || 'Subscription error'));
77
+ // Error code 105 = "already subscribed" (server-side subscription from JWT)
78
+ // This is not an error - the channel is already active via server-side subscription
79
+ if (ctx.error?.code === 105) {
80
+ // This is fine, server-side subscription exists
81
+ } else {
82
+ console.error(`Subscription error for ${this.replyChannel}:`, ctx.error);
83
+ }
61
84
  });
62
85
 
63
86
  this.subscription.subscribe();
@@ -65,9 +88,14 @@ export class CentrifugoRPCClient {
65
88
  }
66
89
 
67
90
  async disconnect(): Promise<void> {
91
+ // Unsubscribe from all event channels
92
+ this.unsubscribeAll();
93
+
94
+ // Unsubscribe from RPC reply channel
68
95
  if (this.subscription) {
69
96
  this.subscription.unsubscribe();
70
97
  }
98
+
71
99
  this.centrifuge.disconnect();
72
100
  }
73
101
 
@@ -103,21 +131,17 @@ export class CentrifugoRPCClient {
103
131
  // Publish request
104
132
  await this.centrifuge.publish('rpc.requests', message);
105
133
 
106
- console.log(`📤 RPC call: ${method} (${correlationId})`);
107
-
108
134
  return promise;
109
135
  }
110
136
 
111
137
  private handleResponse(data: any): void {
112
138
  const correlationId = data.correlation_id;
113
139
  if (!correlationId) {
114
- console.warn('Received response without correlation_id');
115
140
  return;
116
141
  }
117
142
 
118
143
  const pending = this.pendingRequests.get(correlationId);
119
144
  if (!pending) {
120
- console.warn(`Received response for unknown correlation_id: ${correlationId}`);
121
145
  return;
122
146
  }
123
147
 
@@ -126,7 +150,6 @@ export class CentrifugoRPCClient {
126
150
  if (data.error) {
127
151
  pending.reject(new Error(data.error.message || 'RPC error'));
128
152
  } else {
129
- console.log(`📥 RPC response: ${correlationId}`);
130
153
  pending.resolve(data.result);
131
154
  }
132
155
  }
@@ -134,4 +157,120 @@ export class CentrifugoRPCClient {
134
157
  private generateCorrelationId(): string {
135
158
  return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
136
159
  }
160
+
161
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
162
+ // Channel Subscription API (for gRPC events)
163
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
164
+
165
+ /**
166
+ * Subscribe to a Centrifugo channel for real-time events.
167
+ *
168
+ * @param channel - Channel name (e.g., 'bot#bot-123#heartbeat')
169
+ * @param callback - Callback for received messages
170
+ * @returns Unsubscribe function
171
+ *
172
+ * @example
173
+ * const unsubscribe = client.subscribe('bot#bot-123#heartbeat', (data) => {
174
+ * console.log('Heartbeat:', data);
175
+ * });
176
+ *
177
+ * // Later: unsubscribe when done
178
+ * unsubscribe();
179
+ */
180
+ subscribe(channel: string, callback: (data: any) => void): () => void {
181
+ // Check if already subscribed
182
+ if (this.channelSubscriptions.has(channel)) {
183
+ return () => {}; // Return no-op unsubscribe
184
+ }
185
+
186
+ // Create new subscription
187
+ const sub = this.centrifuge.newSubscription(channel);
188
+
189
+ // Handle publications
190
+ sub.on('publication', (ctx: any) => {
191
+ callback(ctx.data);
192
+ });
193
+
194
+ // Handle subscription lifecycle
195
+ sub.on('subscribed', () => {
196
+ // Subscription successful
197
+ });
198
+
199
+ sub.on('error', (ctx: any) => {
200
+ console.error(`Subscription error for ${channel}:`, ctx.error);
201
+ });
202
+
203
+ // Start subscription
204
+ sub.subscribe();
205
+
206
+ // Store subscription
207
+ this.channelSubscriptions.set(channel, sub);
208
+
209
+ // Return unsubscribe function
210
+ return () => this.unsubscribe(channel);
211
+ }
212
+
213
+ /**
214
+ * Unsubscribe from a channel.
215
+ *
216
+ * @param channel - Channel name
217
+ */
218
+ unsubscribe(channel: string): void {
219
+ const sub = this.channelSubscriptions.get(channel);
220
+ if (!sub) {
221
+ return;
222
+ }
223
+
224
+ sub.unsubscribe();
225
+ this.channelSubscriptions.delete(channel);
226
+ }
227
+
228
+ /**
229
+ * Unsubscribe from all channels.
230
+ */
231
+ unsubscribeAll(): void {
232
+ if (this.channelSubscriptions.size === 0) {
233
+ return;
234
+ }
235
+
236
+ this.channelSubscriptions.forEach((sub, channel) => {
237
+ sub.unsubscribe();
238
+ });
239
+ this.channelSubscriptions.clear();
240
+ }
241
+
242
+ /**
243
+ * Get list of active client-side subscriptions.
244
+ */
245
+ getActiveSubscriptions(): string[] {
246
+ return Array.from(this.channelSubscriptions.keys());
247
+ }
248
+
249
+ /**
250
+ * Get list of server-side subscriptions (from JWT token).
251
+ *
252
+ * These are channels automatically subscribed by Centrifugo server
253
+ * based on the 'channels' claim in the JWT token.
254
+ */
255
+ getServerSideSubscriptions(): string[] {
256
+ try {
257
+ // Access Centrifuge.js internal state for server-side subs
258
+ // @ts-ignore - accessing internal property
259
+ const serverSubs = this.centrifuge._serverSubs || {};
260
+ return Object.keys(serverSubs);
261
+ } catch (error) {
262
+ return [];
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Get all active subscriptions (both client-side and server-side).
268
+ */
269
+ getAllSubscriptions(): string[] {
270
+ const clientSubs = this.getActiveSubscriptions();
271
+ const serverSubs = this.getServerSideSubscriptions();
272
+
273
+ // Combine and deduplicate
274
+ return Array.from(new Set([...clientSubs, ...serverSubs]));
275
+ }
137
276
  }
@@ -6,6 +6,7 @@ Usage:
6
6
  python manage.py generate_centrifugo_clients -o ./clients --python --verbose
7
7
  """
8
8
 
9
+ import logging
9
10
  from pathlib import Path
10
11
  from typing import List
11
12
 
@@ -131,11 +132,10 @@ class Command(AdminCommand):
131
132
  if not methods:
132
133
  self.stdout.write(
133
134
  colorize(
134
- "No RPC methods found. Did you register handlers with @websocket_rpc?",
135
+ "⚠️ No RPC methods found. Will generate base RPC client without API methods.",
135
136
  fg="yellow",
136
137
  )
137
138
  )
138
- return
139
139
 
140
140
  # Create output directory
141
141
  output_dir.mkdir(parents=True, exist_ok=True)
@@ -5,8 +5,14 @@ Business logic layer for Centrifugo integration.
5
5
  """
6
6
 
7
7
  from .config_helper import get_centrifugo_config, get_centrifugo_config_or_default
8
+ from .publisher import CentrifugoPublisher, get_centrifugo_publisher
9
+ from .token_generator import get_user_channels, generate_centrifugo_token
8
10
 
9
11
  __all__ = [
10
12
  "get_centrifugo_config",
11
13
  "get_centrifugo_config_or_default",
14
+ "CentrifugoPublisher",
15
+ "get_centrifugo_publisher",
16
+ "get_user_channels",
17
+ "generate_centrifugo_token",
12
18
  ]
@@ -1,11 +1,14 @@
1
1
  """
2
2
  Centrifugo Client.
3
3
 
4
- Django client for publishing messages to Centrifugo via Python Wrapper.
4
+ Two client implementations:
5
+ - CentrifugoClient: Via wrapper (for external API, with auth & logging)
6
+ - DirectCentrifugoClient: Direct to Centrifugo (for internal use, lightweight)
5
7
  """
6
8
 
7
9
  from .client import CentrifugoClient, PublishResponse, get_centrifugo_client
8
10
  from .config import DjangoCfgCentrifugoConfig
11
+ from .direct_client import DirectCentrifugoClient, get_direct_centrifugo_client
9
12
  from .exceptions import (
10
13
  CentrifugoBaseException,
11
14
  CentrifugoConfigurationError,
@@ -19,6 +22,8 @@ __all__ = [
19
22
  "DjangoCfgCentrifugoConfig",
20
23
  "CentrifugoClient",
21
24
  "get_centrifugo_client",
25
+ "DirectCentrifugoClient",
26
+ "get_direct_centrifugo_client",
22
27
  "PublishResponse",
23
28
  "CentrifugoBaseException",
24
29
  "CentrifugoTimeoutError",
@@ -396,7 +396,7 @@ class CentrifugoClient:
396
396
 
397
397
  error_code = type(last_error).__name__ if last_error else "unknown"
398
398
  error_message = str(last_error) if last_error else "Unknown error"
399
- CentrifugoLogger.mark_failed(
399
+ await CentrifugoLogger.mark_failed_async(
400
400
  log_entry,
401
401
  error_code=error_code,
402
402
  error_message=error_message,