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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +2 -2
- django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -116
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +259 -0
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +56 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +315 -26
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
- django_cfg/apps/integrations/grpc/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/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/transformers.py +89 -0
- 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} +67 -54
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/services/interceptors/centrifugo.py +541 -0
- 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/SERVER_LOGGING.md +164 -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 +261 -13
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -1
- django_cfg/apps/system/dashboard/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/frontend/views.py +87 -6
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -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/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/groups/manager.py +25 -18
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +12 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +0 -39
- django_cfg/utils/pool_monitor.py +320 -0
- django_cfg/utils/smart_defaults.py +233 -7
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/RECORD +118 -74
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.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.14.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
- {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
|
+
]
|
django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py}
RENAMED
|
@@ -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
|
|
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
|
|
14
|
-
from
|
|
15
|
-
from .
|
|
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 .
|
|
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
|
|
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
|
+
]
|