django-cfg 1.5.20__py3-none-any.whl → 1.5.31__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/integrations/centrifugo/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/logging.py +90 -14
- django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +47 -43
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +41 -29
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +1 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +22 -36
- 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/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/demo.py +1 -1
- django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/test_publish.py +4 -4
- 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} +62 -55
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +216 -5
- 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/__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 +55 -8
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -1
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -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/files_generator.py +12 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +22 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/validation-events.ts.jinja +133 -0
- django_cfg/modules/django_client/core/groups/manager.py +25 -18
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_client/urls.py +38 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/modules/django_twilio/email_otp.py +3 -1
- django_cfg/modules/django_twilio/sms.py +3 -1
- django_cfg/modules/django_twilio/unified.py +6 -2
- django_cfg/modules/django_twilio/whatsapp.py +3 -1
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +17 -57
- django_cfg/utils/pool_monitor.py +320 -0
- django_cfg/utils/smart_defaults.py +233 -7
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/METADATA +75 -5
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/RECORD +97 -68
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.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.20.dist-info → django_cfg-1.5.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.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
|
+
]
|