django-cfg 1.5.20__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 (88) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
  3. django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
  4. django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
  5. django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
  6. django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -37
  7. django_cfg/apps/integrations/centrifugo/views/wrapper.py +25 -23
  8. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
  9. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +1 -1
  10. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +21 -36
  11. django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
  12. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
  13. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
  14. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
  15. django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
  16. django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
  17. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/demo.py +1 -1
  18. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/test_publish.py +4 -4
  19. django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
  20. django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
  21. django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
  22. django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
  23. django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
  24. django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
  25. django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
  26. django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
  27. django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
  28. django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
  29. django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
  30. django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
  31. django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
  32. django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +62 -55
  33. django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
  34. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
  35. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
  36. django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
  37. django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
  38. django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
  39. django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
  40. django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
  41. django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
  42. django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
  43. django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
  44. django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
  45. django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
  46. django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
  47. django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
  48. django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
  49. django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
  50. django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
  51. django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
  52. django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
  53. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +55 -8
  54. django_cfg/apps/integrations/grpc/views/charts.py +1 -1
  55. django_cfg/apps/integrations/grpc/views/config.py +1 -1
  56. django_cfg/core/base/config_model.py +11 -0
  57. django_cfg/core/builders/middleware_builder.py +5 -0
  58. django_cfg/management/commands/pool_status.py +153 -0
  59. django_cfg/middleware/pool_cleanup.py +261 -0
  60. django_cfg/models/api/grpc/config.py +2 -2
  61. django_cfg/models/infrastructure/database/config.py +16 -0
  62. django_cfg/models/infrastructure/database/converters.py +2 -0
  63. django_cfg/modules/django_admin/utils/html/composition.py +57 -13
  64. django_cfg/modules/django_admin/utils/html_builder.py +1 -0
  65. django_cfg/modules/django_client/core/groups/manager.py +25 -18
  66. django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
  67. django_cfg/modules/django_logging/django_logger.py +58 -19
  68. django_cfg/pyproject.toml +3 -3
  69. django_cfg/static/frontend/admin.zip +0 -0
  70. django_cfg/templates/admin/index.html +0 -39
  71. django_cfg/utils/pool_monitor.py +320 -0
  72. django_cfg/utils/smart_defaults.py +233 -7
  73. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
  74. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/RECORD +87 -59
  75. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
  76. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
  77. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
  78. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
  79. /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
  80. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
  81. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
  82. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
  83. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
  84. /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
  85. /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
  86. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
  87. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
  88. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,430 @@
1
+ """
2
+ Cross-process command routing for gRPC services.
3
+
4
+ This module provides automatic routing between direct calls (same process)
5
+ and gRPC calls (cross-process) for Django multi-process architecture.
6
+
7
+ **Problem**:
8
+ Django typically runs multiple processes:
9
+ - `runserver` - HTTP server process
10
+ - `rungrpc` - gRPC server process (with active WebSocket connections)
11
+
12
+ When code in `runserver` needs to send a command to a connected gRPC client,
13
+ it must use gRPC to communicate with `rungrpc` process.
14
+
15
+ **Solution**:
16
+ CrossProcessCommandRouter automatically detects the current process and routes commands:
17
+ 1. **Same process (rungrpc)**: Direct method call (fast, no network)
18
+ 2. **Different process (runserver)**: gRPC call to localhost (cross-process)
19
+
20
+ **Architecture**:
21
+ ```
22
+ ┌─────────────────────────────────────────────────────────────┐
23
+ │ runserver process │
24
+ │ │
25
+ │ Django View/Command │
26
+ │ │ │
27
+ │ ▼ │
28
+ │ CrossProcessCommandRouter │
29
+ │ │ │
30
+ │ │ (service_instance is None) │
31
+ │ │ │
32
+ │ ▼ │
33
+ │ gRPC call to localhost:50051 ────────────────────┐ │
34
+ └───────────────────────────────────────────────────│─────────┘
35
+
36
+ │ gRPC
37
+
38
+ ┌────────────────────────────────────────────────────┼─────────┐
39
+ │ rungrpc process │ │
40
+ │ ▼ │
41
+ │ RPC Handler (SendCommandToClient) │
42
+ │ │ │
43
+ │ ▼ │
44
+ │ service_instance.send_to_client() │
45
+ │ │ │
46
+ │ ▼ │
47
+ │ Active WebSocket connection │
48
+ └──────────────────────────────────────────────────────────────┘
49
+ ```
50
+
51
+ **Usage Example**:
52
+ ```python
53
+ from .router import CrossProcessCommandRouter, CrossProcessConfig
54
+
55
+ # 1. Configure router
56
+ config = CrossProcessConfig(
57
+ grpc_host="localhost",
58
+ grpc_port=50051,
59
+ rpc_method_name="SendCommandToClient",
60
+ timeout=5.0,
61
+ )
62
+
63
+ router = CrossProcessCommandRouter(
64
+ config=config,
65
+ get_service_instance=lambda: get_streaming_service(),
66
+ )
67
+
68
+ # 2. Register with service (in rungrpc process)
69
+ streaming_service = BotStreamingService()
70
+ router.register_service(streaming_service)
71
+
72
+ # 3. Route commands (works from any process)
73
+ success = await router.send_command(
74
+ client_id="bot_123",
75
+ command=my_command_protobuf,
76
+ )
77
+ ```
78
+
79
+ Created: 2025-11-07
80
+ Status: %%PRODUCTION%%
81
+ Phase: Phase 1 - Universal Components
82
+ """
83
+
84
+ from typing import Generic, Optional, Callable, Any, TypeVar
85
+ import logging
86
+
87
+ import grpc
88
+
89
+ from .config import CrossProcessConfig
90
+
91
+
92
+ logger = logging.getLogger(__name__)
93
+
94
+
95
+ # ============================================================================
96
+ # Type Variables
97
+ # ============================================================================
98
+
99
+ TCommand = TypeVar('TCommand')
100
+ """Generic type for command messages (protobuf)"""
101
+
102
+ TService = TypeVar('TService')
103
+ """Generic type for service instance"""
104
+
105
+ TRequest = TypeVar('TRequest')
106
+ """Generic type for gRPC request"""
107
+
108
+ TResponse = TypeVar('TResponse')
109
+ """Generic type for gRPC response"""
110
+
111
+
112
+ # ============================================================================
113
+ # Router
114
+ # ============================================================================
115
+
116
+ class CrossProcessCommandRouter(Generic[TCommand, TService]):
117
+ """
118
+ Routes commands between direct calls and cross-process gRPC calls.
119
+
120
+ This router automatically detects whether the service instance is available
121
+ locally (same process) or requires cross-process communication via gRPC.
122
+
123
+ **Type Parameters**:
124
+ TCommand: Type of command to route (protobuf message)
125
+ TService: Type of service instance
126
+
127
+ **Parameters**:
128
+ config: CrossProcessConfig with gRPC connection details
129
+ get_service_instance: Callable that returns local service instance (or None)
130
+ stub_factory: Factory function to create gRPC stub from channel
131
+ request_factory: Factory function to create gRPC request
132
+ extract_success: Function to extract success bool from response
133
+
134
+ **Example - Full Setup**:
135
+ ```python
136
+ # 1. Define factories
137
+ def create_stub(channel):
138
+ return BotStreamingServiceStub(channel)
139
+
140
+ def create_request(client_id, command):
141
+ return SendCommandRequest(
142
+ client_id=client_id,
143
+ command=command,
144
+ )
145
+
146
+ def is_success(response):
147
+ return response.success
148
+
149
+ # 2. Create router
150
+ router = CrossProcessCommandRouter(
151
+ config=config,
152
+ get_service_instance=lambda: _streaming_service_instance,
153
+ stub_factory=create_stub,
154
+ request_factory=create_request,
155
+ extract_success=is_success,
156
+ )
157
+
158
+ # 3. Use anywhere (automatically routes correctly)
159
+ success = await router.send_command("bot_123", command_pb)
160
+ ```
161
+ """
162
+
163
+ def __init__(
164
+ self,
165
+ config: CrossProcessConfig,
166
+ get_service_instance: Callable[[], Optional[TService]],
167
+ stub_factory: Callable[[grpc.aio.Channel], Any],
168
+ request_factory: Callable[[str, TCommand], Any],
169
+ extract_success: Callable[[Any], bool],
170
+ extract_message: Optional[Callable[[Any], str]] = None,
171
+ ):
172
+ """
173
+ Initialize cross-process command router.
174
+
175
+ Args:
176
+ config: Pydantic configuration
177
+ get_service_instance: Returns local service instance or None
178
+ stub_factory: Creates gRPC stub from channel
179
+ request_factory: Creates gRPC request from (client_id, command)
180
+ extract_success: Extracts success bool from gRPC response
181
+ extract_message: Optional - extracts error message from response
182
+ """
183
+ self.config = config
184
+ self.get_service_instance = get_service_instance
185
+ self.stub_factory = stub_factory
186
+ self.request_factory = request_factory
187
+ self.extract_success = extract_success
188
+ self.extract_message = extract_message or (lambda r: getattr(r, 'message', ''))
189
+
190
+ if self.config.enable_logging:
191
+ logger.info(
192
+ f"CrossProcessCommandRouter initialized: {self.config.grpc_address}, "
193
+ f"method={self.config.rpc_method_name}"
194
+ )
195
+
196
+ # ------------------------------------------------------------------------
197
+ # Main Routing Method
198
+ # ------------------------------------------------------------------------
199
+
200
+ async def send_command(
201
+ self,
202
+ client_id: str,
203
+ command: TCommand,
204
+ ) -> bool:
205
+ """
206
+ Send command to client (automatically routes).
207
+
208
+ **Routing Logic**:
209
+ 1. Check if service instance is available locally
210
+ 2. If yes -> direct call (fast, same process)
211
+ 3. If no -> gRPC call to localhost (cross-process)
212
+
213
+ Args:
214
+ client_id: Target client identifier
215
+ command: Command to send (protobuf message)
216
+
217
+ Returns:
218
+ True if command sent successfully, False otherwise
219
+
220
+ Example:
221
+ ```python
222
+ # Works from any process!
223
+ command = BotCommand(action="START")
224
+ success = await router.send_command("bot_123", command)
225
+ ```
226
+ """
227
+ # Try direct call first (same process)
228
+ service = self.get_service_instance()
229
+
230
+ if service is not None:
231
+ return await self._send_direct(service, client_id, command)
232
+
233
+ # Fallback to cross-process call
234
+ return await self._send_cross_process(client_id, command)
235
+
236
+ # ------------------------------------------------------------------------
237
+ # Direct Call (Same Process)
238
+ # ------------------------------------------------------------------------
239
+
240
+ async def _send_direct(
241
+ self,
242
+ service: TService,
243
+ client_id: str,
244
+ command: TCommand,
245
+ ) -> bool:
246
+ """
247
+ Send command via direct method call (same process).
248
+
249
+ Args:
250
+ service: Local service instance
251
+ client_id: Target client ID
252
+ command: Command to send
253
+
254
+ Returns:
255
+ True if sent successfully
256
+ """
257
+ if self.config.enable_logging:
258
+ logger.debug(f"📞 Direct call for client {client_id}")
259
+
260
+ try:
261
+ # Assumes service has send_to_client method
262
+ # (from BidirectionalStreamingService)
263
+ success = await service.send_to_client(client_id, command)
264
+
265
+ if self.config.enable_logging:
266
+ if success:
267
+ logger.info(f"✅ Direct call succeeded for {client_id}")
268
+ else:
269
+ logger.warning(f"⚠️ Direct call failed for {client_id} (client not connected)")
270
+
271
+ return success
272
+
273
+ except Exception as e:
274
+ if self.config.enable_logging:
275
+ logger.error(f"❌ Direct call error for {client_id}: {e}", exc_info=True)
276
+ return False
277
+
278
+ # ------------------------------------------------------------------------
279
+ # Cross-Process Call (gRPC)
280
+ # ------------------------------------------------------------------------
281
+
282
+ async def _send_cross_process(
283
+ self,
284
+ client_id: str,
285
+ command: TCommand,
286
+ ) -> bool:
287
+ """
288
+ Send command via gRPC call to localhost (cross-process).
289
+
290
+ Args:
291
+ client_id: Target client ID
292
+ command: Command to send
293
+
294
+ Returns:
295
+ True if sent successfully
296
+ """
297
+ if self.config.enable_logging:
298
+ logger.debug(
299
+ f"📡 Cross-process gRPC call for client {client_id} to {self.config.grpc_address}"
300
+ )
301
+
302
+ try:
303
+ # Create gRPC channel to local server
304
+ async with grpc.aio.insecure_channel(self.config.grpc_address) as channel:
305
+ # Create stub
306
+ stub = self.stub_factory(channel)
307
+
308
+ # Get RPC method dynamically
309
+ rpc_method = getattr(stub, self.config.rpc_method_name)
310
+
311
+ # Create request
312
+ request = self.request_factory(client_id, command)
313
+
314
+ # Call RPC with timeout
315
+ response = await rpc_method(
316
+ request,
317
+ timeout=self.config.timeout,
318
+ )
319
+
320
+ # Extract success
321
+ success = self.extract_success(response)
322
+
323
+ if self.config.enable_logging:
324
+ if success:
325
+ logger.info(f"✅ Cross-process RPC succeeded for {client_id}")
326
+ else:
327
+ message = self.extract_message(response)
328
+ logger.warning(f"⚠️ Cross-process RPC failed for {client_id}: {message}")
329
+
330
+ return success
331
+
332
+ except grpc.RpcError as e:
333
+ if self.config.enable_logging:
334
+ logger.error(
335
+ f"❌ gRPC error for {client_id}: {e.code()} - {e.details()}",
336
+ exc_info=True,
337
+ )
338
+ return False
339
+
340
+ except Exception as e:
341
+ if self.config.enable_logging:
342
+ logger.error(f"❌ Cross-process call error for {client_id}: {e}", exc_info=True)
343
+ return False
344
+
345
+ # ------------------------------------------------------------------------
346
+ # Broadcast
347
+ # ------------------------------------------------------------------------
348
+
349
+ async def broadcast_command(
350
+ self,
351
+ command: TCommand,
352
+ client_ids: Optional[list[str]] = None,
353
+ ) -> dict[str, bool]:
354
+ """
355
+ Broadcast command to multiple clients.
356
+
357
+ Args:
358
+ command: Command to broadcast
359
+ client_ids: Optional list of client IDs (None = all connected)
360
+
361
+ Returns:
362
+ Dict mapping client_id -> success bool
363
+
364
+ Example:
365
+ ```python
366
+ results = await router.broadcast_command(
367
+ command=shutdown_command,
368
+ client_ids=["bot_1", "bot_2", "bot_3"],
369
+ )
370
+ # {"bot_1": True, "bot_2": False, "bot_3": True}
371
+ ```
372
+ """
373
+ results = {}
374
+
375
+ # If no client_ids provided, try to get all from service
376
+ if client_ids is None:
377
+ service = self.get_service_instance()
378
+ if service is not None and hasattr(service, 'get_active_connections'):
379
+ client_ids = list(service.get_active_connections().keys())
380
+ else:
381
+ if self.config.enable_logging:
382
+ logger.warning("Cannot broadcast: no client_ids and service unavailable")
383
+ return {}
384
+
385
+ # Send to each client
386
+ for client_id in client_ids:
387
+ success = await self.send_command(client_id, command)
388
+ results[client_id] = success
389
+
390
+ if self.config.enable_logging:
391
+ success_count = sum(results.values())
392
+ total_count = len(results)
393
+ logger.info(f"Broadcast completed: {success_count}/{total_count} succeeded")
394
+
395
+ return results
396
+
397
+ # ------------------------------------------------------------------------
398
+ # Utilities
399
+ # ------------------------------------------------------------------------
400
+
401
+ def is_same_process(self) -> bool:
402
+ """
403
+ Check if running in same process as gRPC server.
404
+
405
+ Returns:
406
+ True if service instance is available (same process)
407
+ """
408
+ return self.get_service_instance() is not None
409
+
410
+ def get_process_mode(self) -> str:
411
+ """
412
+ Get current process mode.
413
+
414
+ Returns:
415
+ "direct" if same process, "cross-process" otherwise
416
+ """
417
+ return "direct" if self.is_same_process() else "cross-process"
418
+
419
+
420
+ # ============================================================================
421
+ # Exports
422
+ # ============================================================================
423
+
424
+ __all__ = [
425
+ # Config
426
+ 'CrossProcessConfig',
427
+
428
+ # Router
429
+ 'CrossProcessCommandRouter',
430
+ ]
@@ -0,0 +1,117 @@
1
+ """
2
+ Universal bidirectional streaming components for gRPC.
3
+
4
+ This package provides generic, type-safe components for implementing
5
+ bidirectional gRPC streaming services.
6
+
7
+ **Components**:
8
+ - types: Protocol definitions for type-safe callbacks
9
+ - config: Pydantic v2 configuration models
10
+ - service: BidirectionalStreamingService implementation
11
+
12
+ **Usage Example**:
13
+ ```python
14
+ from django_cfg.apps.integrations.grpc.services.streaming import (
15
+ BidirectionalStreamingService,
16
+ BidirectionalStreamingConfig,
17
+ ConfigPresets,
18
+ MessageProcessor,
19
+ ClientIdExtractor,
20
+ PingMessageCreator,
21
+ )
22
+
23
+ # Use preset config
24
+ service = BidirectionalStreamingService(
25
+ config=ConfigPresets.PRODUCTION,
26
+ message_processor=my_processor,
27
+ client_id_extractor=extract_id,
28
+ ping_message_creator=create_ping,
29
+ )
30
+ ```
31
+
32
+ Created: 2025-11-07
33
+ Status: %%PRODUCTION%%
34
+ Phase: Phase 1 - Universal Components
35
+ """
36
+
37
+ # Type definitions
38
+ from .types import (
39
+ # Type variables
40
+ TMessage,
41
+ TCommand,
42
+
43
+ # Core protocols
44
+ MessageProcessor,
45
+ ClientIdExtractor,
46
+ PingMessageCreator,
47
+
48
+ # Connection protocols
49
+ ConnectionCallback,
50
+ ErrorHandler,
51
+
52
+ # Type aliases
53
+ MessageProcessorType,
54
+ ClientIdExtractorType,
55
+ PingMessageCreatorType,
56
+
57
+ # Validation
58
+ is_valid_message_processor,
59
+ is_valid_client_id_extractor,
60
+ is_valid_ping_creator,
61
+ )
62
+
63
+ # Configuration
64
+ from .config import (
65
+ # Enums
66
+ StreamingMode,
67
+ PingStrategy,
68
+
69
+ # Models
70
+ BidirectionalStreamingConfig,
71
+
72
+ # Presets
73
+ ConfigPresets,
74
+ )
75
+
76
+ # Service - lazy import to avoid grpc dependency
77
+ def __getattr__(name):
78
+ """Lazy import BidirectionalStreamingService to avoid grpc dependency."""
79
+ if name == 'BidirectionalStreamingService':
80
+ from .service import BidirectionalStreamingService
81
+ return BidirectionalStreamingService
82
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
83
+
84
+
85
+ __all__ = [
86
+ # Type variables
87
+ 'TMessage',
88
+ 'TCommand',
89
+
90
+ # Protocols
91
+ 'MessageProcessor',
92
+ 'ClientIdExtractor',
93
+ 'PingMessageCreator',
94
+ 'ConnectionCallback',
95
+ 'ErrorHandler',
96
+
97
+ # Type aliases
98
+ 'MessageProcessorType',
99
+ 'ClientIdExtractorType',
100
+ 'PingMessageCreatorType',
101
+
102
+ # Validation functions
103
+ 'is_valid_message_processor',
104
+ 'is_valid_client_id_extractor',
105
+ 'is_valid_ping_creator',
106
+
107
+ # Enums
108
+ 'StreamingMode',
109
+ 'PingStrategy',
110
+
111
+ # Configuration
112
+ 'BidirectionalStreamingConfig',
113
+ 'ConfigPresets',
114
+
115
+ # Service
116
+ 'BidirectionalStreamingService',
117
+ ]