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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
- 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 +2 -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/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/logging.py +47 -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/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -116
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +259 -0
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +56 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +315 -26
- 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/grpc_request_log.py +84 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
- django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
- django_cfg/apps/integrations/grpc/services/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
- django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
- django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
- django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
- django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
- django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
- django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
- django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +67 -54
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/services/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
- django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
- django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
- django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
- django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
- django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
- django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
- django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
- django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
- django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
- django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
- django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
- django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
- django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
- django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
- django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +261 -13
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -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/frontend/views.py +87 -6
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -0
- django_cfg/management/commands/pool_status.py +153 -0
- django_cfg/middleware/pool_cleanup.py +261 -0
- django_cfg/models/api/grpc/config.py +2 -2
- django_cfg/models/infrastructure/database/config.py +16 -0
- django_cfg/models/infrastructure/database/converters.py +2 -0
- django_cfg/modules/django_admin/utils/html/composition.py +57 -13
- django_cfg/modules/django_admin/utils/html_builder.py +1 -0
- 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/groups/manager.py +25 -18
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +12 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +0 -39
- django_cfg/utils/pool_monitor.py +320 -0
- django_cfg/utils/smart_defaults.py +233 -7
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/RECORD +118 -74
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
- /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
73
|
+
// Subscription successful (optional, we already resolved on 'connected')
|
|
57
74
|
});
|
|
58
75
|
|
|
59
76
|
this.subscription.on('error', (ctx: any) => {
|
|
60
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
399
|
+
await CentrifugoLogger.mark_failed_async(
|
|
400
400
|
log_entry,
|
|
401
401
|
error_code=error_code,
|
|
402
402
|
error_message=error_message,
|