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,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
+ ]