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
@@ -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
+ ]