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,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Streaming Commands
|
|
3
|
+
|
|
4
|
+
Provides reusable command client architecture for bidirectional gRPC streaming services.
|
|
5
|
+
|
|
6
|
+
Quick Start:
|
|
7
|
+
1. Create your command client:
|
|
8
|
+
from django_cfg.apps.integrations.grpc.services.commands.base import StreamingCommandClient
|
|
9
|
+
from your_app.grpc import service_pb2 as pb2
|
|
10
|
+
|
|
11
|
+
class MyCommandClient(StreamingCommandClient[pb2.Command]):
|
|
12
|
+
async def _send_via_grpc(self, command):
|
|
13
|
+
# Implement gRPC call
|
|
14
|
+
async with grpc.aio.insecure_channel(self.get_grpc_address()) as channel:
|
|
15
|
+
stub = service_pb2_grpc.YourServiceStub(channel)
|
|
16
|
+
request = pb2.SendCommandRequest(client_id=self.client_id, command=command)
|
|
17
|
+
response = await stub.SendCommandToClient(request)
|
|
18
|
+
return response.success
|
|
19
|
+
|
|
20
|
+
2. Register your streaming service:
|
|
21
|
+
from django_cfg.apps.integrations.grpc.services.commands.registry import register_streaming_service
|
|
22
|
+
|
|
23
|
+
def grpc_handlers(server):
|
|
24
|
+
servicer = YourService()
|
|
25
|
+
register_streaming_service("your_service", servicer._streaming_service)
|
|
26
|
+
# ...
|
|
27
|
+
|
|
28
|
+
3. Use the client:
|
|
29
|
+
# Cross-process mode
|
|
30
|
+
client = MyCommandClient(client_id="123", grpc_port=50051)
|
|
31
|
+
await client.send_command(command)
|
|
32
|
+
|
|
33
|
+
# Same-process mode
|
|
34
|
+
from django_cfg.apps.integrations.grpc.services.commands.registry import get_streaming_service
|
|
35
|
+
service = get_streaming_service("your_service")
|
|
36
|
+
client = MyCommandClient(client_id="123", streaming_service=service)
|
|
37
|
+
await client.send_command(command)
|
|
38
|
+
|
|
39
|
+
Documentation:
|
|
40
|
+
See @commands/ directory for complete documentation:
|
|
41
|
+
- README.md: Overview and quick start
|
|
42
|
+
- ARCHITECTURE.md: System design
|
|
43
|
+
- EXAMPLES.md: Code examples
|
|
44
|
+
- INDEX.md: Navigation hub
|
|
45
|
+
|
|
46
|
+
Version: 1.0.0
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from .base import (
|
|
50
|
+
StreamingCommandClient,
|
|
51
|
+
CommandClientConfig,
|
|
52
|
+
CommandError,
|
|
53
|
+
CommandTimeoutError,
|
|
54
|
+
ClientNotConnectedError,
|
|
55
|
+
TCommand,
|
|
56
|
+
)
|
|
57
|
+
from .registry import (
|
|
58
|
+
register_streaming_service,
|
|
59
|
+
get_streaming_service,
|
|
60
|
+
unregister_streaming_service,
|
|
61
|
+
list_streaming_services,
|
|
62
|
+
is_registered,
|
|
63
|
+
clear_registry,
|
|
64
|
+
set_streaming_service,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
__version__ = "1.0.0"
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
# Base classes
|
|
71
|
+
'StreamingCommandClient',
|
|
72
|
+
'CommandClientConfig',
|
|
73
|
+
|
|
74
|
+
# Exceptions
|
|
75
|
+
'CommandError',
|
|
76
|
+
'CommandTimeoutError',
|
|
77
|
+
'ClientNotConnectedError',
|
|
78
|
+
|
|
79
|
+
# Registry functions
|
|
80
|
+
'register_streaming_service',
|
|
81
|
+
'get_streaming_service',
|
|
82
|
+
'unregister_streaming_service',
|
|
83
|
+
'list_streaming_services',
|
|
84
|
+
'is_registered',
|
|
85
|
+
'clear_registry',
|
|
86
|
+
'set_streaming_service',
|
|
87
|
+
|
|
88
|
+
# Type variables
|
|
89
|
+
'TCommand',
|
|
90
|
+
|
|
91
|
+
# Version
|
|
92
|
+
'__version__',
|
|
93
|
+
]
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Streaming Command Client - Base Implementation
|
|
3
|
+
|
|
4
|
+
This module provides a generic, reusable command client for bidirectional gRPC streaming services.
|
|
5
|
+
|
|
6
|
+
Key Features:
|
|
7
|
+
- Dual-mode: Same-process (direct queue) or Cross-process (gRPC RPC)
|
|
8
|
+
- Type-safe: Generic[TCommand] for different protobuf types
|
|
9
|
+
- Auto-detection: Automatically chooses the right mode
|
|
10
|
+
- Minimal coupling: Works with any BidirectionalStreamingService
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from your_app.grpc.commands.base import StreamingCommandClient
|
|
14
|
+
from your_app.grpc import your_service_pb2 as pb2
|
|
15
|
+
|
|
16
|
+
class YourCommandClient(StreamingCommandClient[pb2.Command]):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
Documentation: See @commands/README.md for complete guide
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import logging
|
|
24
|
+
from abc import ABC
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Generic, Optional, TypeVar, Any
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import grpc
|
|
30
|
+
GRPC_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
GRPC_AVAILABLE = False
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# Generic type for protobuf command messages
|
|
37
|
+
TCommand = TypeVar('TCommand')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CommandClientConfig:
|
|
42
|
+
"""Configuration for command client behavior."""
|
|
43
|
+
|
|
44
|
+
# Queue timeout for same-process mode (seconds)
|
|
45
|
+
queue_timeout: float = 5.0
|
|
46
|
+
|
|
47
|
+
# gRPC connection timeout for cross-process mode (seconds)
|
|
48
|
+
connect_timeout: float = 3.0
|
|
49
|
+
|
|
50
|
+
# gRPC call timeout (seconds)
|
|
51
|
+
call_timeout: float = 5.0
|
|
52
|
+
|
|
53
|
+
# Default gRPC server address
|
|
54
|
+
grpc_host: str = "localhost"
|
|
55
|
+
|
|
56
|
+
# Default gRPC server port (can be overridden)
|
|
57
|
+
grpc_port: Optional[int] = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class StreamingCommandClient(Generic[TCommand], ABC):
|
|
61
|
+
"""
|
|
62
|
+
Universal command client for bidirectional streaming services.
|
|
63
|
+
|
|
64
|
+
Supports two modes:
|
|
65
|
+
1. Same-process: Direct queue access when streaming_service is provided
|
|
66
|
+
2. Cross-process: gRPC RPC call when streaming_service is None
|
|
67
|
+
|
|
68
|
+
Type Parameters:
|
|
69
|
+
TCommand: The protobuf message type for commands
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
# Same-process mode
|
|
73
|
+
from your_app.grpc.services.registry import get_streaming_service
|
|
74
|
+
|
|
75
|
+
service = get_streaming_service("your_service")
|
|
76
|
+
client = YourCommandClient(
|
|
77
|
+
client_id="client-123",
|
|
78
|
+
streaming_service=service
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Cross-process mode
|
|
82
|
+
client = YourCommandClient(
|
|
83
|
+
client_id="client-123",
|
|
84
|
+
grpc_port=50051
|
|
85
|
+
)
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
client_id: str,
|
|
91
|
+
streaming_service: Optional[Any] = None,
|
|
92
|
+
config: Optional[CommandClientConfig] = None,
|
|
93
|
+
grpc_port: Optional[int] = None,
|
|
94
|
+
grpc_host: Optional[str] = None,
|
|
95
|
+
):
|
|
96
|
+
"""
|
|
97
|
+
Initialize command client.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
client_id: Unique identifier for the client
|
|
101
|
+
streaming_service: BidirectionalStreamingService instance for same-process mode
|
|
102
|
+
config: Configuration object (uses defaults if not provided)
|
|
103
|
+
grpc_port: Override gRPC port for cross-process mode
|
|
104
|
+
grpc_host: Override gRPC host for cross-process mode
|
|
105
|
+
"""
|
|
106
|
+
self.client_id = client_id
|
|
107
|
+
self._streaming_service = streaming_service
|
|
108
|
+
self.config = config or CommandClientConfig()
|
|
109
|
+
|
|
110
|
+
# Override config with provided values
|
|
111
|
+
if grpc_port is not None:
|
|
112
|
+
self.config.grpc_port = grpc_port
|
|
113
|
+
if grpc_host is not None:
|
|
114
|
+
self.config.grpc_host = grpc_host
|
|
115
|
+
|
|
116
|
+
# Determine mode
|
|
117
|
+
self._is_same_process = streaming_service is not None
|
|
118
|
+
|
|
119
|
+
logger.debug(
|
|
120
|
+
f"Initialized {self.__class__.__name__} for client_id={client_id}, "
|
|
121
|
+
f"mode={'same-process' if self._is_same_process else 'cross-process'}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def _send_command(self, command: TCommand) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Send command to client (auto-detects mode).
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
command: Protobuf command message
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True if command was sent successfully, False otherwise
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
RuntimeError: If gRPC is not available in cross-process mode
|
|
136
|
+
"""
|
|
137
|
+
if self._is_same_process:
|
|
138
|
+
return await self._send_direct(command)
|
|
139
|
+
else:
|
|
140
|
+
return await self._send_via_grpc(command)
|
|
141
|
+
|
|
142
|
+
async def _send_direct(self, command: TCommand) -> bool:
|
|
143
|
+
"""
|
|
144
|
+
Send command directly via queue (same-process mode).
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
command: Protobuf command message
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if command was queued, False if client not connected
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
success = await self._streaming_service.send_to_client(
|
|
154
|
+
client_id=self.client_id,
|
|
155
|
+
command=command,
|
|
156
|
+
timeout=self.config.queue_timeout
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if success:
|
|
160
|
+
logger.debug(f"Command sent to {self.client_id} (same-process)")
|
|
161
|
+
else:
|
|
162
|
+
logger.warning(f"Client {self.client_id} not connected")
|
|
163
|
+
|
|
164
|
+
return success
|
|
165
|
+
|
|
166
|
+
except asyncio.TimeoutError:
|
|
167
|
+
logger.error(
|
|
168
|
+
f"Timeout sending command to {self.client_id} "
|
|
169
|
+
f"(timeout={self.config.queue_timeout}s)"
|
|
170
|
+
)
|
|
171
|
+
return False
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(
|
|
174
|
+
f"Error sending command to {self.client_id}: {e}",
|
|
175
|
+
exc_info=True
|
|
176
|
+
)
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
async def _send_via_grpc(self, command: TCommand) -> bool:
|
|
180
|
+
"""
|
|
181
|
+
Send command via gRPC RPC (cross-process mode).
|
|
182
|
+
|
|
183
|
+
This method should be overridden in subclasses to implement
|
|
184
|
+
the actual gRPC call with service-specific stub and request.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
command: Protobuf command message
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True if RPC succeeded, False otherwise
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
NotImplementedError: Must be implemented in subclass
|
|
194
|
+
RuntimeError: If gRPC is not available
|
|
195
|
+
"""
|
|
196
|
+
raise NotImplementedError(
|
|
197
|
+
f"{self.__class__.__name__} must implement _send_via_grpc() "
|
|
198
|
+
"for cross-process communication. See EXAMPLES.md for reference."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def is_same_process(self) -> bool:
|
|
202
|
+
"""Check if running in same-process mode."""
|
|
203
|
+
return self._is_same_process
|
|
204
|
+
|
|
205
|
+
def get_grpc_address(self) -> str:
|
|
206
|
+
"""Get gRPC server address for cross-process mode."""
|
|
207
|
+
if self.config.grpc_port is None:
|
|
208
|
+
# Try to auto-detect from django-cfg config
|
|
209
|
+
try:
|
|
210
|
+
from django_cfg.core.config import get_current_config
|
|
211
|
+
self.config.grpc_port = get_current_config().grpc.port
|
|
212
|
+
logger.debug(f"Auto-detected gRPC port: {self.config.grpc_port}")
|
|
213
|
+
except Exception as e:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
f"grpc_port not configured and auto-detection failed: {e}. "
|
|
216
|
+
"Either set it in config or pass to __init__"
|
|
217
|
+
)
|
|
218
|
+
return f"{self.config.grpc_host}:{self.config.grpc_port}"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class CommandError(Exception):
|
|
222
|
+
"""Base exception for command-related errors."""
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class CommandTimeoutError(CommandError):
|
|
227
|
+
"""Raised when command send times out."""
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class ClientNotConnectedError(CommandError):
|
|
232
|
+
"""Raised when client is not connected."""
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
__all__ = [
|
|
237
|
+
'StreamingCommandClient',
|
|
238
|
+
'CommandClientConfig',
|
|
239
|
+
'CommandError',
|
|
240
|
+
'CommandTimeoutError',
|
|
241
|
+
'ClientNotConnectedError',
|
|
242
|
+
'TCommand',
|
|
243
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example Command Implementations
|
|
3
|
+
|
|
4
|
+
This package contains example command implementations that demonstrate
|
|
5
|
+
the universal streaming command pattern.
|
|
6
|
+
|
|
7
|
+
These examples show:
|
|
8
|
+
- How to decompose commands into separate modules
|
|
9
|
+
- How to use Protocol for type-safe model operations
|
|
10
|
+
- How to implement Django async ORM updates
|
|
11
|
+
- How to build a wrapper client class
|
|
12
|
+
|
|
13
|
+
To use these examples in your project:
|
|
14
|
+
1. Copy the pattern from base_client.py
|
|
15
|
+
2. Adapt command functions (start.py, stop.py, config.py) to your needs
|
|
16
|
+
3. Create a wrapper client class (client.py)
|
|
17
|
+
4. Update protobuf imports to match your service
|
|
18
|
+
|
|
19
|
+
See @commands/EXAMPLES.md for detailed documentation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__all__ = []
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: Base Command Client Implementation
|
|
3
|
+
|
|
4
|
+
This example shows how to extend StreamingCommandClient for a specific gRPC service.
|
|
5
|
+
It implements the _send_via_grpc method for cross-process communication.
|
|
6
|
+
|
|
7
|
+
Copy this pattern and adapt it to your service:
|
|
8
|
+
1. Update protobuf imports (your_service_pb2, your_service_pb2_grpc)
|
|
9
|
+
2. Update Command type to match your proto
|
|
10
|
+
3. Update stub class name
|
|
11
|
+
4. Update SendCommandRequest message structure
|
|
12
|
+
5. Optionally add model type hints using Protocol
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
# Import your extended client
|
|
16
|
+
from your_app.grpc.commands.base_client import ExampleCommandClient
|
|
17
|
+
|
|
18
|
+
# Cross-process mode
|
|
19
|
+
client = ExampleCommandClient(client_id="123", grpc_port=50051)
|
|
20
|
+
|
|
21
|
+
# Same-process mode
|
|
22
|
+
from django_cfg.apps.integrations.grpc.services.commands.registry import get_streaming_service
|
|
23
|
+
service = get_streaming_service("example")
|
|
24
|
+
client = ExampleCommandClient(client_id="123", streaming_service=service)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Any, Optional, Protocol, runtime_checkable
|
|
29
|
+
|
|
30
|
+
from django_cfg.apps.integrations.grpc.services.commands.base import (
|
|
31
|
+
StreamingCommandClient,
|
|
32
|
+
CommandClientConfig,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# IMPORTANT: Update these imports to match your protobuf files
|
|
36
|
+
# Example assumes you have:
|
|
37
|
+
# - your_service_pb2.py (generated from your_service.proto)
|
|
38
|
+
# - your_service_pb2_grpc.py (generated gRPC stub)
|
|
39
|
+
#
|
|
40
|
+
# from your_app.grpc.generated import your_service_pb2 as pb2
|
|
41
|
+
# from your_app.grpc.generated import your_service_pb2_grpc as pb2_grpc
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# Protocol for Type-Safe Model Operations
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
@runtime_checkable
|
|
51
|
+
class HasStatus(Protocol):
|
|
52
|
+
"""
|
|
53
|
+
Protocol for models that have a status field.
|
|
54
|
+
|
|
55
|
+
This allows type-safe operations on Django models without tight coupling.
|
|
56
|
+
Any model with these attributes/methods will pass type checking.
|
|
57
|
+
|
|
58
|
+
Example Django model:
|
|
59
|
+
class YourModel(models.Model):
|
|
60
|
+
status = models.CharField(max_length=20)
|
|
61
|
+
|
|
62
|
+
# Django 5.2+ async save
|
|
63
|
+
async def asave(self, update_fields=None):
|
|
64
|
+
...
|
|
65
|
+
"""
|
|
66
|
+
status: str
|
|
67
|
+
|
|
68
|
+
async def asave(self, update_fields: Optional[list[str]] = None) -> None:
|
|
69
|
+
"""Django async save method (Django 5.2+)."""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@runtime_checkable
|
|
74
|
+
class HasConfig(Protocol):
|
|
75
|
+
"""
|
|
76
|
+
Protocol for models that have configuration.
|
|
77
|
+
|
|
78
|
+
Example Django model:
|
|
79
|
+
class YourModel(models.Model):
|
|
80
|
+
config = models.JSONField(default=dict)
|
|
81
|
+
|
|
82
|
+
async def asave(self, update_fields=None):
|
|
83
|
+
...
|
|
84
|
+
"""
|
|
85
|
+
config: dict
|
|
86
|
+
|
|
87
|
+
async def asave(self, update_fields: Optional[list[str]] = None) -> None:
|
|
88
|
+
"""Django async save method."""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ============================================================================
|
|
93
|
+
# Example Command Client
|
|
94
|
+
# ============================================================================
|
|
95
|
+
|
|
96
|
+
class ExampleCommandClient(StreamingCommandClient): # [pb2.Command]
|
|
97
|
+
"""
|
|
98
|
+
Example implementation of universal command client.
|
|
99
|
+
|
|
100
|
+
This shows how to extend StreamingCommandClient with gRPC implementation.
|
|
101
|
+
|
|
102
|
+
Type Parameters:
|
|
103
|
+
TCommand: Should be your protobuf Command type (e.g., pb2.Command)
|
|
104
|
+
|
|
105
|
+
Note: The type parameter is commented out because protobuf imports
|
|
106
|
+
are not available in this example. In your implementation, use:
|
|
107
|
+
|
|
108
|
+
class YourCommandClient(StreamingCommandClient[pb2.Command]):
|
|
109
|
+
...
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
client_id: str,
|
|
115
|
+
model: Optional[Any] = None,
|
|
116
|
+
streaming_service: Optional[Any] = None,
|
|
117
|
+
config: Optional[CommandClientConfig] = None,
|
|
118
|
+
grpc_port: Optional[int] = None,
|
|
119
|
+
grpc_host: Optional[str] = None,
|
|
120
|
+
):
|
|
121
|
+
"""
|
|
122
|
+
Initialize example command client.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
client_id: Unique identifier for the client
|
|
126
|
+
model: Optional Django model instance (for callbacks)
|
|
127
|
+
streaming_service: BidirectionalStreamingService for same-process mode
|
|
128
|
+
config: Client configuration
|
|
129
|
+
grpc_port: gRPC port for cross-process mode
|
|
130
|
+
grpc_host: gRPC host for cross-process mode
|
|
131
|
+
"""
|
|
132
|
+
super().__init__(
|
|
133
|
+
client_id=client_id,
|
|
134
|
+
streaming_service=streaming_service,
|
|
135
|
+
config=config,
|
|
136
|
+
grpc_port=grpc_port,
|
|
137
|
+
grpc_host=grpc_host,
|
|
138
|
+
)
|
|
139
|
+
self.model = model
|
|
140
|
+
|
|
141
|
+
async def _send_via_grpc(self, command) -> bool: # command: pb2.Command
|
|
142
|
+
"""
|
|
143
|
+
Send command via gRPC RPC (cross-process mode).
|
|
144
|
+
|
|
145
|
+
This implements the abstract method from StreamingCommandClient.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
command: Protobuf command message (pb2.Command)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
True if RPC succeeded, False otherwise
|
|
152
|
+
|
|
153
|
+
Implementation guide:
|
|
154
|
+
1. Import grpc and your stubs at the top of the file:
|
|
155
|
+
import grpc
|
|
156
|
+
from your_app.grpc.generated import your_service_pb2_grpc as pb2_grpc
|
|
157
|
+
from your_app.grpc.generated import your_service_pb2 as pb2
|
|
158
|
+
|
|
159
|
+
2. Create gRPC channel
|
|
160
|
+
3. Create stub instance
|
|
161
|
+
4. Create request message with client_id and command
|
|
162
|
+
5. Call SendCommandToClient RPC
|
|
163
|
+
6. Return response.success
|
|
164
|
+
"""
|
|
165
|
+
# This is a template - you need to implement with your actual protobuf types
|
|
166
|
+
|
|
167
|
+
logger.warning(
|
|
168
|
+
"Example implementation: _send_via_grpc is not implemented. "
|
|
169
|
+
"Please copy this file and implement with your actual protobuf types."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Example implementation (uncomment and adapt):
|
|
173
|
+
#
|
|
174
|
+
# try:
|
|
175
|
+
# import grpc
|
|
176
|
+
#
|
|
177
|
+
# async with grpc.aio.insecure_channel(
|
|
178
|
+
# self.get_grpc_address(),
|
|
179
|
+
# options=[
|
|
180
|
+
# ('grpc.keepalive_time_ms', 10000),
|
|
181
|
+
# ('grpc.keepalive_timeout_ms', 5000),
|
|
182
|
+
# ]
|
|
183
|
+
# ) as channel:
|
|
184
|
+
# stub = pb2_grpc.YourServiceStub(channel)
|
|
185
|
+
#
|
|
186
|
+
# # Create request with your service's message structure
|
|
187
|
+
# request = pb2.SendCommandRequest(
|
|
188
|
+
# client_id=self.client_id,
|
|
189
|
+
# command=command,
|
|
190
|
+
# )
|
|
191
|
+
#
|
|
192
|
+
# # Call your service's SendCommandToClient RPC
|
|
193
|
+
# response = await stub.SendCommandToClient(
|
|
194
|
+
# request,
|
|
195
|
+
# timeout=self.config.call_timeout
|
|
196
|
+
# )
|
|
197
|
+
#
|
|
198
|
+
# if response.success:
|
|
199
|
+
# logger.debug(f"Command sent to {self.client_id} via gRPC")
|
|
200
|
+
# return True
|
|
201
|
+
# else:
|
|
202
|
+
# logger.warning(
|
|
203
|
+
# f"Command failed for {self.client_id}: {response.message}"
|
|
204
|
+
# )
|
|
205
|
+
# return False
|
|
206
|
+
#
|
|
207
|
+
# except grpc.RpcError as e:
|
|
208
|
+
# logger.error(
|
|
209
|
+
# f"gRPC error sending command to {self.client_id}: "
|
|
210
|
+
# f"{e.code()} - {e.details()}",
|
|
211
|
+
# exc_info=True
|
|
212
|
+
# )
|
|
213
|
+
# return False
|
|
214
|
+
# except Exception as e:
|
|
215
|
+
# logger.error(
|
|
216
|
+
# f"Error sending command to {self.client_id}: {e}",
|
|
217
|
+
# exc_info=True
|
|
218
|
+
# )
|
|
219
|
+
# return False
|
|
220
|
+
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
__all__ = [
|
|
225
|
+
'ExampleCommandClient',
|
|
226
|
+
'HasStatus',
|
|
227
|
+
'HasConfig',
|
|
228
|
+
]
|