django-cfg 1.5.14__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/business/accounts/serializers/profile.py +42 -0
- 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 +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/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/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/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/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +55 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +311 -7
- 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/services/discovery.py +7 -1
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +206 -5
- 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/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -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/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +12 -0
- django_cfg/pyproject.toml +1 -1
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/RECORD +53 -37
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Demo Service for Testing CentrifugoInterceptor + CentrifugoBridgeMixin.
|
|
3
|
+
|
|
4
|
+
This module provides a complete working example of how to use both
|
|
5
|
+
Centrifugo integration mechanisms:
|
|
6
|
+
1. CentrifugoInterceptor - Automatic RPC metadata publishing
|
|
7
|
+
2. CentrifugoBridgeMixin - Service-level message data publishing
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
# Send single demo event
|
|
11
|
+
>>> await send_demo_event()
|
|
12
|
+
|
|
13
|
+
# Test mixin-based publishing
|
|
14
|
+
>>> await test_demo_service()
|
|
15
|
+
|
|
16
|
+
# Test complete chain (interceptor + mixin)
|
|
17
|
+
>>> await test_complete_integration()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Optional, Any, Dict
|
|
25
|
+
from datetime import datetime, timezone as tz
|
|
26
|
+
|
|
27
|
+
from google.protobuf.struct_pb2 import Struct
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
from rich.panel import Panel
|
|
30
|
+
from rich.table import Table
|
|
31
|
+
|
|
32
|
+
from .bridge import CentrifugoBridgeMixin
|
|
33
|
+
from .config import ChannelConfig, CentrifugoChannels
|
|
34
|
+
from ..utils.streaming_logger import setup_streaming_logger
|
|
35
|
+
|
|
36
|
+
# Setup logger with Rich support
|
|
37
|
+
logger = setup_streaming_logger(
|
|
38
|
+
name='centrifugo_demo',
|
|
39
|
+
level=logging.DEBUG,
|
|
40
|
+
console_level=logging.INFO
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Rich console for beautiful output
|
|
44
|
+
console = Console()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ========================================================================
|
|
48
|
+
# Demo Channel Configuration (Pydantic)
|
|
49
|
+
# ========================================================================
|
|
50
|
+
|
|
51
|
+
class DemoChannels(CentrifugoChannels):
|
|
52
|
+
"""
|
|
53
|
+
Demo channel configuration for testing CentrifugoBridgeMixin.
|
|
54
|
+
|
|
55
|
+
This shows how to define channel mappings using Pydantic v2 models.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# High-frequency updates (rate limited)
|
|
59
|
+
heartbeat: ChannelConfig = ChannelConfig(
|
|
60
|
+
template='demo#{service_id}#heartbeat',
|
|
61
|
+
rate_limit=0.5, # Max once per 0.5 seconds
|
|
62
|
+
metadata={'priority': 'low', 'demo': True}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Status changes (critical, always published)
|
|
66
|
+
status: ChannelConfig = ChannelConfig(
|
|
67
|
+
template='demo#{service_id}#status',
|
|
68
|
+
critical=True, # Bypass rate limiting
|
|
69
|
+
metadata={'priority': 'high', 'demo': True}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Test execution reports
|
|
73
|
+
execution: ChannelConfig = ChannelConfig(
|
|
74
|
+
template='demo#{service_id}#executions',
|
|
75
|
+
rate_limit=1.0,
|
|
76
|
+
metadata={'priority': 'medium', 'demo': True}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Error events (critical)
|
|
80
|
+
error: ChannelConfig = ChannelConfig(
|
|
81
|
+
template='demo#{service_id}#errors',
|
|
82
|
+
critical=True,
|
|
83
|
+
metadata={'priority': 'critical', 'demo': True}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ========================================================================
|
|
88
|
+
# Demo Service with CentrifugoBridgeMixin
|
|
89
|
+
# ========================================================================
|
|
90
|
+
|
|
91
|
+
class DemoBridgeService(CentrifugoBridgeMixin):
|
|
92
|
+
"""
|
|
93
|
+
Demo service using CentrifugoBridgeMixin for Centrifugo publishing.
|
|
94
|
+
|
|
95
|
+
This demonstrates:
|
|
96
|
+
- Pydantic configuration
|
|
97
|
+
- Automatic field detection
|
|
98
|
+
- Rate limiting
|
|
99
|
+
- Critical event bypass
|
|
100
|
+
- Template-based channel naming
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> service = DemoBridgeService()
|
|
104
|
+
>>> message = create_mock_message('heartbeat', cpu=45.2)
|
|
105
|
+
>>> await service.publish_demo(message, service_id='demo-123')
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
# Configure Centrifugo channels using Pydantic
|
|
109
|
+
centrifugo_channels = DemoChannels()
|
|
110
|
+
|
|
111
|
+
def __init__(self):
|
|
112
|
+
"""Initialize demo service with Centrifugo bridge."""
|
|
113
|
+
super().__init__()
|
|
114
|
+
logger.info("DemoBridgeService initialized with Centrifugo bridge")
|
|
115
|
+
|
|
116
|
+
async def publish_demo(
|
|
117
|
+
self,
|
|
118
|
+
message: Struct,
|
|
119
|
+
service_id: str,
|
|
120
|
+
**extra_context: Any
|
|
121
|
+
) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
Publish demo message to Centrifugo.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
message: Protobuf Struct message with test data
|
|
127
|
+
service_id: Service identifier for channel routing
|
|
128
|
+
**extra_context: Additional template variables
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True if published successfully
|
|
132
|
+
"""
|
|
133
|
+
context = {'service_id': service_id, **extra_context}
|
|
134
|
+
return await self._notify_centrifugo(message, **context)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ========================================================================
|
|
138
|
+
# Mock Protobuf Message Generation
|
|
139
|
+
# ========================================================================
|
|
140
|
+
|
|
141
|
+
def create_mock_message(message_type: str, **fields: Any) -> Struct:
|
|
142
|
+
"""
|
|
143
|
+
Create a mock protobuf Struct message for testing.
|
|
144
|
+
|
|
145
|
+
Uses google.protobuf.struct_pb2.Struct to simulate protobuf messages
|
|
146
|
+
without requiring compiled .proto files.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
message_type: Type of message (heartbeat, status, execution, error)
|
|
150
|
+
**fields: Additional fields to include in the message
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Protobuf Struct message with HasField() support
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
>>> msg = create_mock_message('heartbeat', cpu=45.2, memory=60.1)
|
|
157
|
+
>>> msg.HasField('heartbeat')
|
|
158
|
+
True
|
|
159
|
+
"""
|
|
160
|
+
message = Struct()
|
|
161
|
+
|
|
162
|
+
# Create nested structure for the message type
|
|
163
|
+
field_data = message.fields[message_type]
|
|
164
|
+
|
|
165
|
+
# Add timestamp by default
|
|
166
|
+
field_data.struct_value.fields['timestamp'].string_value = (
|
|
167
|
+
datetime.now(tz.utc).isoformat()
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Add custom fields
|
|
171
|
+
for key, value in fields.items():
|
|
172
|
+
if isinstance(value, bool):
|
|
173
|
+
field_data.struct_value.fields[key].bool_value = value
|
|
174
|
+
elif isinstance(value, (int, float)):
|
|
175
|
+
field_data.struct_value.fields[key].number_value = float(value)
|
|
176
|
+
elif isinstance(value, str):
|
|
177
|
+
field_data.struct_value.fields[key].string_value = value
|
|
178
|
+
elif value is None:
|
|
179
|
+
field_data.struct_value.fields[key].null_value = 0
|
|
180
|
+
else:
|
|
181
|
+
field_data.struct_value.fields[key].string_value = str(value)
|
|
182
|
+
|
|
183
|
+
return message
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def generate_mock_heartbeat(**overrides: Any) -> Struct:
|
|
187
|
+
"""Generate mock heartbeat message."""
|
|
188
|
+
defaults = {
|
|
189
|
+
'status': 'RUNNING',
|
|
190
|
+
'cpu_usage': 45.2,
|
|
191
|
+
'memory_usage': 60.1,
|
|
192
|
+
'open_positions': 3,
|
|
193
|
+
'daily_pnl': 1250.50,
|
|
194
|
+
}
|
|
195
|
+
defaults.update(overrides)
|
|
196
|
+
return create_mock_message('heartbeat', **defaults)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def generate_mock_status(**overrides: Any) -> Struct:
|
|
200
|
+
"""Generate mock status update message."""
|
|
201
|
+
defaults = {
|
|
202
|
+
'old_status': 'STOPPED',
|
|
203
|
+
'new_status': 'RUNNING',
|
|
204
|
+
'reason': 'Demo test event',
|
|
205
|
+
}
|
|
206
|
+
defaults.update(overrides)
|
|
207
|
+
return create_mock_message('status', **defaults)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def generate_mock_execution(**overrides: Any) -> Struct:
|
|
211
|
+
"""Generate mock execution report message."""
|
|
212
|
+
defaults = {
|
|
213
|
+
'execution_id': f'exec-{datetime.now().microsecond}',
|
|
214
|
+
'symbol': 'BTCUSDT',
|
|
215
|
+
'side': 'BUY',
|
|
216
|
+
'quantity': 0.01,
|
|
217
|
+
'price': 50000.0,
|
|
218
|
+
'status': 'FILLED',
|
|
219
|
+
}
|
|
220
|
+
defaults.update(overrides)
|
|
221
|
+
return create_mock_message('execution', **defaults)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def generate_mock_error(**overrides: Any) -> Struct:
|
|
225
|
+
"""Generate mock error report message."""
|
|
226
|
+
defaults = {
|
|
227
|
+
'error_code': 'DEMO_ERROR',
|
|
228
|
+
'message': 'This is a test error',
|
|
229
|
+
'severity': 'WARNING',
|
|
230
|
+
}
|
|
231
|
+
defaults.update(overrides)
|
|
232
|
+
return create_mock_message('error', **defaults)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ========================================================================
|
|
236
|
+
# Testing Functions
|
|
237
|
+
# ========================================================================
|
|
238
|
+
|
|
239
|
+
async def send_demo_event(
|
|
240
|
+
channel: str = "grpc#demo#TestMethod#meta",
|
|
241
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
242
|
+
quiet: bool = False,
|
|
243
|
+
) -> bool:
|
|
244
|
+
"""
|
|
245
|
+
Send a raw demo event to Centrifugo (interceptor simulation).
|
|
246
|
+
|
|
247
|
+
This simulates what CentrifugoInterceptor publishes automatically.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
channel: Centrifugo channel name
|
|
251
|
+
metadata: Additional metadata to include
|
|
252
|
+
quiet: Suppress log messages
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
True if published successfully
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
>>> await send_demo_event()
|
|
259
|
+
✅ Demo event published to: grpc#demo#TestMethod#meta
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
from django_cfg.apps.integrations.centrifugo.services import get_centrifugo_publisher
|
|
263
|
+
|
|
264
|
+
publisher = get_centrifugo_publisher()
|
|
265
|
+
|
|
266
|
+
# Use high-level publishing service
|
|
267
|
+
await publisher.publish_demo_event(
|
|
268
|
+
channel=channel,
|
|
269
|
+
metadata={
|
|
270
|
+
"method": "/demo.DemoService/TestMethod",
|
|
271
|
+
"service": "demo.DemoService",
|
|
272
|
+
"method_name": "TestMethod",
|
|
273
|
+
"peer": "demo-client",
|
|
274
|
+
"duration_ms": 123.45,
|
|
275
|
+
"status": "OK",
|
|
276
|
+
"message": "Raw interceptor-style event from demo.py",
|
|
277
|
+
**(metadata or {}),
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if not quiet:
|
|
282
|
+
logger.info(f"✅ Demo event published to: {channel}")
|
|
283
|
+
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
except Exception as e:
|
|
287
|
+
if not quiet:
|
|
288
|
+
logger.error(f"❌ Failed to send demo event: {e}")
|
|
289
|
+
return False
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def test_demo_service(
|
|
293
|
+
service_id: str = "demo-service-123",
|
|
294
|
+
count: int = 3,
|
|
295
|
+
) -> Dict[str, int]:
|
|
296
|
+
"""
|
|
297
|
+
Test DemoBridgeService with mock messages (mixin testing).
|
|
298
|
+
|
|
299
|
+
This tests the CentrifugoBridgeMixin functionality with various
|
|
300
|
+
message types and rate limiting.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
service_id: Service identifier for channels
|
|
304
|
+
count: Number of test messages per type
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Dict with publish statistics
|
|
308
|
+
|
|
309
|
+
Example:
|
|
310
|
+
>>> stats = await test_demo_service(count=5)
|
|
311
|
+
>>> print(stats)
|
|
312
|
+
{'heartbeat': 3, 'status': 5, 'execution': 3, 'error': 5}
|
|
313
|
+
"""
|
|
314
|
+
logger.info(f"🧪 Testing DemoBridgeService with service_id={service_id}")
|
|
315
|
+
|
|
316
|
+
service = DemoBridgeService()
|
|
317
|
+
|
|
318
|
+
stats = {
|
|
319
|
+
'heartbeat': 0,
|
|
320
|
+
'status': 0,
|
|
321
|
+
'execution': 0,
|
|
322
|
+
'error': 0,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Test different message types
|
|
326
|
+
for i in range(1, count + 1):
|
|
327
|
+
logger.info(f"--- Test iteration {i}/{count} ---")
|
|
328
|
+
|
|
329
|
+
# Test heartbeat (rate limited)
|
|
330
|
+
heartbeat = generate_mock_heartbeat(
|
|
331
|
+
cpu_usage=40.0 + i * 5,
|
|
332
|
+
memory_usage=50.0 + i * 3,
|
|
333
|
+
)
|
|
334
|
+
if await service.publish_demo(heartbeat, service_id=service_id):
|
|
335
|
+
stats['heartbeat'] += 1
|
|
336
|
+
logger.info(f" ✅ Heartbeat {i} published")
|
|
337
|
+
else:
|
|
338
|
+
logger.info(f" ⏱️ Heartbeat {i} rate limited")
|
|
339
|
+
|
|
340
|
+
# Test status (critical, always published)
|
|
341
|
+
status = generate_mock_status(
|
|
342
|
+
new_status=f"TEST_STATE_{i}"
|
|
343
|
+
)
|
|
344
|
+
if await service.publish_demo(status, service_id=service_id):
|
|
345
|
+
stats['status'] += 1
|
|
346
|
+
logger.info(f" ✅ Status {i} published (critical)")
|
|
347
|
+
|
|
348
|
+
# Test execution (rate limited)
|
|
349
|
+
execution = generate_mock_execution(
|
|
350
|
+
price=50000.0 + i * 100,
|
|
351
|
+
)
|
|
352
|
+
if await service.publish_demo(execution, service_id=service_id):
|
|
353
|
+
stats['execution'] += 1
|
|
354
|
+
logger.info(f" ✅ Execution {i} published")
|
|
355
|
+
else:
|
|
356
|
+
logger.info(f" ⏱️ Execution {i} rate limited")
|
|
357
|
+
|
|
358
|
+
# Test error (critical, always published)
|
|
359
|
+
error = generate_mock_error(
|
|
360
|
+
error_code=f"TEST_ERROR_{i}"
|
|
361
|
+
)
|
|
362
|
+
if await service.publish_demo(error, service_id=service_id):
|
|
363
|
+
stats['error'] += 1
|
|
364
|
+
logger.info(f" ✅ Error {i} published (critical)")
|
|
365
|
+
|
|
366
|
+
# Small delay between iterations
|
|
367
|
+
if i < count:
|
|
368
|
+
await asyncio.sleep(0.6) # Slightly longer than heartbeat rate limit
|
|
369
|
+
|
|
370
|
+
logger.info(f"📊 Test complete: {stats}")
|
|
371
|
+
return stats
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
async def test_interceptor_simulation(count: int = 3) -> int:
|
|
375
|
+
"""
|
|
376
|
+
Test interceptor-style event publishing.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
count: Number of events to send
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Number of successfully published events
|
|
383
|
+
"""
|
|
384
|
+
logger.info(f"🔬 Testing CentrifugoInterceptor simulation ({count} events)")
|
|
385
|
+
|
|
386
|
+
success_count = 0
|
|
387
|
+
|
|
388
|
+
for i in range(1, count + 1):
|
|
389
|
+
result = await send_demo_event(
|
|
390
|
+
metadata={
|
|
391
|
+
"sequence_number": i,
|
|
392
|
+
"total_events": count,
|
|
393
|
+
},
|
|
394
|
+
quiet=True,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if result:
|
|
398
|
+
success_count += 1
|
|
399
|
+
logger.info(f"✅ Interceptor event {i}/{count} published")
|
|
400
|
+
else:
|
|
401
|
+
logger.warning(f"⚠️ Interceptor event {i}/{count} failed")
|
|
402
|
+
|
|
403
|
+
if i < count:
|
|
404
|
+
await asyncio.sleep(0.5)
|
|
405
|
+
|
|
406
|
+
logger.info(f"📊 Interceptor test: {success_count}/{count} successful")
|
|
407
|
+
return success_count
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
async def test_complete_integration(
|
|
411
|
+
service_id: str = "demo-integration-test",
|
|
412
|
+
mixin_count: int = 3,
|
|
413
|
+
interceptor_count: int = 3,
|
|
414
|
+
) -> Dict[str, Any]:
|
|
415
|
+
"""
|
|
416
|
+
Test complete integration: both interceptor and mixin.
|
|
417
|
+
|
|
418
|
+
This demonstrates how CentrifugoInterceptor and CentrifugoBridgeMixin
|
|
419
|
+
work together to provide complete event visibility.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
service_id: Service identifier for mixin tests
|
|
423
|
+
mixin_count: Number of mixin test messages
|
|
424
|
+
interceptor_count: Number of interceptor test events
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Complete test results
|
|
428
|
+
|
|
429
|
+
Example:
|
|
430
|
+
>>> results = await test_complete_integration()
|
|
431
|
+
>>> print(results)
|
|
432
|
+
{
|
|
433
|
+
'interceptor': {'published': 3, 'total': 3},
|
|
434
|
+
'mixin': {'heartbeat': 2, 'status': 3, ...},
|
|
435
|
+
'total_published': 15,
|
|
436
|
+
}
|
|
437
|
+
"""
|
|
438
|
+
# Rich header
|
|
439
|
+
console.print()
|
|
440
|
+
console.print(Panel.fit(
|
|
441
|
+
"[bold cyan]Centrifugo Integration Test[/bold cyan]\n"
|
|
442
|
+
"[dim]Testing CentrifugoInterceptor + CentrifugoBridgeMixin[/dim]",
|
|
443
|
+
border_style="cyan"
|
|
444
|
+
))
|
|
445
|
+
|
|
446
|
+
results = {
|
|
447
|
+
'interceptor': {'published': 0, 'total': interceptor_count},
|
|
448
|
+
'mixin': {},
|
|
449
|
+
'total_published': 0,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
# Test 1: Interceptor (RPC metadata)
|
|
453
|
+
console.print("\n[bold blue]📍 Phase 1: CentrifugoInterceptor (RPC metadata)[/bold blue]")
|
|
454
|
+
console.rule(style="blue")
|
|
455
|
+
interceptor_success = await test_interceptor_simulation(count=interceptor_count)
|
|
456
|
+
results['interceptor']['published'] = interceptor_success
|
|
457
|
+
results['total_published'] += interceptor_success
|
|
458
|
+
|
|
459
|
+
# Small delay between phases
|
|
460
|
+
await asyncio.sleep(1.0)
|
|
461
|
+
|
|
462
|
+
# Test 2: Mixin (Message data)
|
|
463
|
+
console.print("\n[bold green]📍 Phase 2: CentrifugoBridgeMixin (message data)[/bold green]")
|
|
464
|
+
console.rule(style="green")
|
|
465
|
+
mixin_stats = await test_demo_service(service_id=service_id, count=mixin_count)
|
|
466
|
+
results['mixin'] = mixin_stats
|
|
467
|
+
results['total_published'] += sum(mixin_stats.values())
|
|
468
|
+
|
|
469
|
+
# Summary table
|
|
470
|
+
console.print()
|
|
471
|
+
console.rule("[bold]Test Summary[/bold]", style="yellow")
|
|
472
|
+
|
|
473
|
+
summary_table = Table(title="📊 Integration Test Results", show_header=True, header_style="bold magenta")
|
|
474
|
+
summary_table.add_column("Component", style="cyan", width=20)
|
|
475
|
+
summary_table.add_column("Published", style="green", justify="right")
|
|
476
|
+
summary_table.add_column("Total", style="blue", justify="right")
|
|
477
|
+
summary_table.add_column("Success Rate", style="yellow", justify="right")
|
|
478
|
+
|
|
479
|
+
# Interceptor row
|
|
480
|
+
interceptor_rate = (results['interceptor']['published'] / results['interceptor']['total'] * 100) if results['interceptor']['total'] > 0 else 0
|
|
481
|
+
summary_table.add_row(
|
|
482
|
+
"Interceptor",
|
|
483
|
+
str(results['interceptor']['published']),
|
|
484
|
+
str(results['interceptor']['total']),
|
|
485
|
+
f"{interceptor_rate:.1f}%"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# Mixin rows
|
|
489
|
+
for msg_type, count in results['mixin'].items():
|
|
490
|
+
rate = (count / mixin_count * 100) if mixin_count > 0 else 0
|
|
491
|
+
summary_table.add_row(
|
|
492
|
+
f"Mixin ({msg_type})",
|
|
493
|
+
str(count),
|
|
494
|
+
str(mixin_count),
|
|
495
|
+
f"{rate:.1f}%"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Total row
|
|
499
|
+
total_expected = results['interceptor']['total'] + (mixin_count * len(results['mixin']))
|
|
500
|
+
total_rate = (results['total_published'] / total_expected * 100) if total_expected > 0 else 0
|
|
501
|
+
summary_table.add_row(
|
|
502
|
+
"[bold]TOTAL[/bold]",
|
|
503
|
+
f"[bold]{results['total_published']}[/bold]",
|
|
504
|
+
f"[bold]{total_expected}[/bold]",
|
|
505
|
+
f"[bold]{total_rate:.1f}%[/bold]",
|
|
506
|
+
style="bold yellow"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
console.print(summary_table)
|
|
510
|
+
|
|
511
|
+
# Status message
|
|
512
|
+
if results['total_published'] == total_expected:
|
|
513
|
+
console.print("\n[bold green]✅ All tests passed successfully![/bold green]")
|
|
514
|
+
elif results['total_published'] > 0:
|
|
515
|
+
console.print(f"\n[bold yellow]⚠️ Partial success: {results['total_published']}/{total_expected} events published[/bold yellow]")
|
|
516
|
+
else:
|
|
517
|
+
console.print("\n[bold red]❌ All tests failed![/bold red]")
|
|
518
|
+
|
|
519
|
+
console.print()
|
|
520
|
+
|
|
521
|
+
return results
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
async def start_demo_publisher(
|
|
525
|
+
interval: float = 5.0,
|
|
526
|
+
duration: Optional[float] = None,
|
|
527
|
+
service_id: str = "demo-continuous",
|
|
528
|
+
) -> None:
|
|
529
|
+
"""
|
|
530
|
+
Start continuous demo event publisher using mixin.
|
|
531
|
+
|
|
532
|
+
Publishes rotating message types until stopped or duration expires.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
interval: Seconds between events
|
|
536
|
+
duration: Total duration in seconds (None = run forever)
|
|
537
|
+
service_id: Service ID for channel routing
|
|
538
|
+
|
|
539
|
+
Example:
|
|
540
|
+
>>> # Run for 30 seconds
|
|
541
|
+
>>> await start_demo_publisher(interval=5.0, duration=30.0)
|
|
542
|
+
|
|
543
|
+
>>> # Run forever (until Ctrl+C)
|
|
544
|
+
>>> await start_demo_publisher(interval=10.0)
|
|
545
|
+
"""
|
|
546
|
+
logger.info(
|
|
547
|
+
f"🚀 Starting demo publisher "
|
|
548
|
+
f"(interval={interval}s, duration={duration or 'infinite'})"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
service = DemoBridgeService()
|
|
552
|
+
start_time = asyncio.get_event_loop().time()
|
|
553
|
+
event_count = 0
|
|
554
|
+
|
|
555
|
+
# Message generators
|
|
556
|
+
generators = {
|
|
557
|
+
'heartbeat': generate_mock_heartbeat,
|
|
558
|
+
'status': generate_mock_status,
|
|
559
|
+
'execution': generate_mock_execution,
|
|
560
|
+
'error': generate_mock_error,
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
message_types = list(generators.keys())
|
|
564
|
+
|
|
565
|
+
try:
|
|
566
|
+
while True:
|
|
567
|
+
event_count += 1
|
|
568
|
+
|
|
569
|
+
# Rotate through message types
|
|
570
|
+
message_type = message_types[(event_count - 1) % len(message_types)]
|
|
571
|
+
|
|
572
|
+
# Generate message
|
|
573
|
+
message = generators[message_type]()
|
|
574
|
+
|
|
575
|
+
# Publish
|
|
576
|
+
await service.publish_demo(
|
|
577
|
+
message,
|
|
578
|
+
service_id=service_id,
|
|
579
|
+
event_count=event_count,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
logger.info(
|
|
583
|
+
f"📤 Event {event_count}: {message_type} → "
|
|
584
|
+
f"demo#{service_id}#{message_type}"
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Check duration
|
|
588
|
+
if duration:
|
|
589
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
590
|
+
if elapsed >= duration:
|
|
591
|
+
logger.info(
|
|
592
|
+
f"⏱️ Duration reached ({duration}s), "
|
|
593
|
+
f"published {event_count} events"
|
|
594
|
+
)
|
|
595
|
+
break
|
|
596
|
+
|
|
597
|
+
await asyncio.sleep(interval)
|
|
598
|
+
|
|
599
|
+
except asyncio.CancelledError:
|
|
600
|
+
logger.info(f"🛑 Demo publisher stopped (published {event_count} events)")
|
|
601
|
+
raise
|
|
602
|
+
|
|
603
|
+
except Exception as e:
|
|
604
|
+
logger.error(f"❌ Demo publisher error: {e}")
|
|
605
|
+
raise
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
__all__ = [
|
|
609
|
+
# Classes
|
|
610
|
+
"DemoChannels",
|
|
611
|
+
"DemoBridgeService",
|
|
612
|
+
|
|
613
|
+
# Mock message generation
|
|
614
|
+
"create_mock_message",
|
|
615
|
+
"generate_mock_heartbeat",
|
|
616
|
+
"generate_mock_status",
|
|
617
|
+
"generate_mock_execution",
|
|
618
|
+
"generate_mock_error",
|
|
619
|
+
|
|
620
|
+
# Testing functions
|
|
621
|
+
"send_demo_event",
|
|
622
|
+
"test_demo_service",
|
|
623
|
+
"test_interceptor_simulation",
|
|
624
|
+
"test_complete_integration",
|
|
625
|
+
"start_demo_publisher",
|
|
626
|
+
]
|