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,38 @@
1
+ """
2
+ gRPC service monitoring and testing utilities.
3
+
4
+ This package provides tools for monitoring gRPC service health,
5
+ performance, and testing service functionality.
6
+
7
+ **Components**:
8
+ - monitoring: Service health monitoring and metrics
9
+ - testing: Testing utilities for gRPC services
10
+
11
+ **Usage Example**:
12
+ ```python
13
+ from django_cfg.apps.integrations.grpc.services.monitoring import (
14
+ MonitoringService,
15
+ TestingService,
16
+ )
17
+
18
+ # Monitor service health
19
+ monitor = MonitoringService()
20
+ health = monitor.check_health()
21
+
22
+ # Test service
23
+ tester = TestingService()
24
+ results = tester.run_tests()
25
+ ```
26
+
27
+ Created: 2025-11-07
28
+ Status: %%PRODUCTION%%
29
+ """
30
+
31
+ # Export when modules are refactored
32
+ # from .monitoring import MonitoringService
33
+ # from .testing import TestingService
34
+
35
+ __all__ = [
36
+ # 'MonitoringService',
37
+ # 'TestingService',
38
+ ]
@@ -12,8 +12,8 @@ from django.db.models import Avg, Count, Max
12
12
  from django.db.models.functions import TruncDay, TruncHour
13
13
  from django_cfg.modules.django_logging import get_logger
14
14
 
15
- from ..models import GRPCRequestLog, GRPCServerStatus
16
- from .config_helper import get_grpc_server_config
15
+ from ...models import GRPCRequestLog, GRPCServerStatus
16
+ from ..management.config_helper import get_grpc_server_config
17
17
 
18
18
  logger = get_logger("grpc.monitoring_service")
19
19
 
@@ -10,9 +10,9 @@ from django.db import models
10
10
  from django.db.models import Count
11
11
  from django_cfg.modules.django_logging import get_logger
12
12
 
13
- from ..models import GRPCRequestLog
14
- from ..testing import get_example
15
- from .service_registry import ServiceRegistryManager
13
+ from ...models import GRPCRequestLog
14
+ from ...testing import get_example
15
+ from ..discovery.registry import ServiceRegistryManager
16
16
 
17
17
  logger = get_logger("grpc.testing_service")
18
18
 
@@ -197,8 +197,8 @@ class TestingService:
197
197
  import grpc
198
198
  from time import time
199
199
 
200
- from .grpc_client import DynamicGRPCClient
201
- from .config_helper import get_grpc_server_config
200
+ from ..client.client import DynamicGRPCClient
201
+ from ..management.config_helper import get_grpc_server_config
202
202
 
203
203
  # Get gRPC server config
204
204
  grpc_config = get_grpc_server_config()
@@ -0,0 +1,27 @@
1
+ """
2
+ gRPC content rendering services.
3
+
4
+ This package provides tools for generating charts, graphs, and other
5
+ visual content for gRPC responses.
6
+
7
+ **Components**:
8
+ - charts: Chart generation service
9
+
10
+ **Usage Example**:
11
+ ```python
12
+ from django_cfg.apps.integrations.grpc.services.rendering import ChartGenerator
13
+
14
+ generator = ChartGenerator()
15
+ chart_data = generator.generate_chart(data=timeseries, chart_type="line")
16
+ ```
17
+
18
+ Created: 2025-11-07
19
+ Status: %%PRODUCTION%%
20
+ """
21
+
22
+ # Export when modules are refactored
23
+ # from .charts import ChartGenerator
24
+
25
+ __all__ = [
26
+ # 'ChartGenerator',
27
+ ]
@@ -13,7 +13,7 @@ from django.db.models import Avg, Count, Max, Min, Q
13
13
  from django.db.models.functions import TruncDay, TruncHour
14
14
  from django.utils import timezone
15
15
 
16
- from ..models import GRPCRequestLog, GRPCServerStatus
16
+ from ...models import GRPCRequestLog, GRPCServerStatus
17
17
 
18
18
 
19
19
  class ChartGeneratorService:
@@ -0,0 +1,59 @@
1
+ """
2
+ Cross-process command routing for Django multi-process architecture.
3
+
4
+ This package provides automatic routing between direct calls (same process)
5
+ and gRPC calls (cross-process) for scenarios where Django runs multiple processes
6
+ (e.g., runserver + rungrpc).
7
+
8
+ **Components**:
9
+ - router: CrossProcessCommandRouter implementation
10
+ - CrossProcessConfig: Pydantic configuration model
11
+
12
+ **Usage Example**:
13
+ ```python
14
+ from django_cfg.apps.integrations.grpc.services.routing import (
15
+ CrossProcessCommandRouter,
16
+ CrossProcessConfig,
17
+ )
18
+
19
+ # Configure router
20
+ config = CrossProcessConfig(
21
+ grpc_host="localhost",
22
+ grpc_port=50051,
23
+ rpc_method_name="SendCommandToClient",
24
+ )
25
+
26
+ # Create router with factories
27
+ router = CrossProcessCommandRouter(
28
+ config=config,
29
+ get_service_instance=lambda: get_global_service(),
30
+ stub_factory=create_grpc_stub,
31
+ request_factory=create_request,
32
+ extract_success=lambda r: r.success,
33
+ )
34
+
35
+ # Route commands (automatically chooses direct vs gRPC)
36
+ await router.send_command("client_123", command_pb)
37
+ ```
38
+
39
+ Created: 2025-11-07
40
+ Status: %%PRODUCTION%%
41
+ Phase: Phase 1 - Universal Components
42
+ """
43
+
44
+ # Config can be imported directly (no grpc dependency)
45
+ from .config import CrossProcessConfig
46
+
47
+ # Lazy import for router (requires grpc)
48
+ def __getattr__(name):
49
+ """Lazy import router to avoid grpc dependency."""
50
+ if name == 'CrossProcessCommandRouter':
51
+ from .router import CrossProcessCommandRouter
52
+ return CrossProcessCommandRouter
53
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
54
+
55
+
56
+ __all__ = [
57
+ 'CrossProcessConfig',
58
+ 'CrossProcessCommandRouter',
59
+ ]
@@ -0,0 +1,76 @@
1
+ """
2
+ Configuration for cross-process command routing.
3
+
4
+ This module provides Pydantic configuration models without gRPC dependencies.
5
+
6
+ Created: 2025-11-07
7
+ Status: %%PRODUCTION%%
8
+ Phase: Phase 1 - Universal Components
9
+ """
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+
14
+ # ============================================================================
15
+ # Configuration
16
+ # ============================================================================
17
+
18
+ class CrossProcessConfig(BaseModel):
19
+ """
20
+ Configuration for cross-process command routing.
21
+
22
+ **Parameters**:
23
+ grpc_host: gRPC server host (usually "localhost")
24
+ grpc_port: gRPC server port
25
+ rpc_method_name: Name of RPC method to call (e.g., "SendCommandToBot")
26
+ timeout: Timeout for gRPC calls in seconds
27
+ enable_logging: Enable detailed logging
28
+
29
+ **Example**:
30
+ ```python
31
+ config = CrossProcessConfig(
32
+ grpc_host="localhost",
33
+ grpc_port=50051,
34
+ rpc_method_name="SendCommandToClient",
35
+ timeout=5.0,
36
+ )
37
+ ```
38
+ """
39
+
40
+ grpc_host: str = Field(
41
+ default="localhost",
42
+ description="gRPC server host",
43
+ )
44
+
45
+ grpc_port: int = Field(
46
+ gt=0,
47
+ le=65535,
48
+ description="gRPC server port (1-65535)",
49
+ )
50
+
51
+ rpc_method_name: str = Field(
52
+ min_length=1,
53
+ description="Name of RPC method for cross-process calls",
54
+ )
55
+
56
+ timeout: float = Field(
57
+ default=5.0,
58
+ gt=0.0,
59
+ le=60.0,
60
+ description="Timeout for gRPC calls in seconds",
61
+ )
62
+
63
+ enable_logging: bool = Field(
64
+ default=True,
65
+ description="Enable detailed logging",
66
+ )
67
+
68
+ model_config = {
69
+ 'extra': 'forbid',
70
+ 'frozen': True,
71
+ }
72
+
73
+ @property
74
+ def grpc_address(self) -> str:
75
+ """Get full gRPC address (host:port)."""
76
+ return f"{self.grpc_host}:{self.grpc_port}"
@@ -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
+ ]