django-cfg 1.5.20__py3-none-any.whl → 1.5.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/logging.py +90 -14
- django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +47 -43
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +41 -29
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +1 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +22 -36
- django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
- django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
- django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/demo.py +1 -1
- django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/test_publish.py +4 -4
- django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
- django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
- django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
- django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
- django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
- django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
- django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
- django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +62 -55
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +216 -5
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
- django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
- django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
- django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
- django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
- django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
- django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
- django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
- django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
- django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
- django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
- django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
- django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
- django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
- django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
- django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +55 -8
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -1
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -0
- django_cfg/management/commands/pool_status.py +153 -0
- django_cfg/middleware/pool_cleanup.py +261 -0
- django_cfg/models/api/grpc/config.py +2 -2
- django_cfg/models/infrastructure/database/config.py +16 -0
- django_cfg/models/infrastructure/database/converters.py +2 -0
- django_cfg/modules/django_admin/utils/html/composition.py +57 -13
- django_cfg/modules/django_admin/utils/html_builder.py +1 -0
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +12 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +22 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/validation-events.ts.jinja +133 -0
- django_cfg/modules/django_client/core/groups/manager.py +25 -18
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_client/urls.py +38 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/modules/django_twilio/email_otp.py +3 -1
- django_cfg/modules/django_twilio/sms.py +3 -1
- django_cfg/modules/django_twilio/unified.py +6 -2
- django_cfg/modules/django_twilio/whatsapp.py +3 -1
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +17 -57
- django_cfg/utils/pool_monitor.py +320 -0
- django_cfg/utils/smart_defaults.py +233 -7
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/METADATA +75 -5
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/RECORD +97 -68
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
- /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: Wrapper Client Class
|
|
3
|
+
|
|
4
|
+
This demonstrates Pattern 3: wrapping command functions in a convenient client class.
|
|
5
|
+
|
|
6
|
+
The wrapper pattern provides:
|
|
7
|
+
- Clean API: client.start() instead of start_client(client, ...)
|
|
8
|
+
- Encapsulation: Commands bundled with client state
|
|
9
|
+
- Convenience: Methods can access self.model, self.client_id automatically
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from your_app.grpc.commands.client import CommandClient
|
|
13
|
+
|
|
14
|
+
# Create client
|
|
15
|
+
client = CommandClient(client_id="123", model=instance, grpc_port=50051)
|
|
16
|
+
|
|
17
|
+
# Use convenient methods
|
|
18
|
+
await client.start(reason="User request")
|
|
19
|
+
await client.update_config()
|
|
20
|
+
await client.stop(graceful=True)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Optional, List, Any
|
|
25
|
+
|
|
26
|
+
from django_cfg.apps.integrations.grpc.services.commands.examples.base_client import (
|
|
27
|
+
ExampleCommandClient,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Import command functions
|
|
31
|
+
from . import start, stop, config
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CommandClient(ExampleCommandClient):
|
|
37
|
+
"""
|
|
38
|
+
Wrapper client class that provides convenient methods for all commands.
|
|
39
|
+
|
|
40
|
+
This wraps the command functions (start.py, stop.py, config.py) into
|
|
41
|
+
class methods for a cleaner API.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
# Cross-process mode (from REST API, signals, tasks)
|
|
45
|
+
from your_app.grpc.commands.client import CommandClient
|
|
46
|
+
|
|
47
|
+
client = CommandClient(
|
|
48
|
+
client_id="bot-123",
|
|
49
|
+
model=bot_instance,
|
|
50
|
+
grpc_port=50051
|
|
51
|
+
)
|
|
52
|
+
await client.start(reason="API request")
|
|
53
|
+
|
|
54
|
+
# Same-process mode (from management commands)
|
|
55
|
+
from your_app.grpc.services.commands.registry import get_streaming_service
|
|
56
|
+
|
|
57
|
+
service = get_streaming_service("bots")
|
|
58
|
+
client = CommandClient(
|
|
59
|
+
client_id="bot-123",
|
|
60
|
+
model=bot_instance,
|
|
61
|
+
streaming_service=service
|
|
62
|
+
)
|
|
63
|
+
await client.start() # Much faster (same-process)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
async def start(self, reason: Optional[str] = None) -> bool:
|
|
67
|
+
"""
|
|
68
|
+
Start the client.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
reason: Optional reason for starting
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if command sent successfully
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
success = await client.start(reason="User requested start")
|
|
78
|
+
"""
|
|
79
|
+
if not self.model:
|
|
80
|
+
logger.error(f"Cannot start {self.client_id}: model not provided")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
return await start.start_client(
|
|
84
|
+
client=self,
|
|
85
|
+
model=self.model,
|
|
86
|
+
reason=reason
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def stop(
|
|
90
|
+
self,
|
|
91
|
+
reason: Optional[str] = None,
|
|
92
|
+
graceful: bool = True
|
|
93
|
+
) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Stop the client.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
reason: Optional reason for stopping
|
|
99
|
+
graceful: If True, allow client to finish current work
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if command sent successfully
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
# Graceful stop
|
|
106
|
+
success = await client.stop(reason="Maintenance", graceful=True)
|
|
107
|
+
|
|
108
|
+
# Force stop
|
|
109
|
+
success = await client.stop(reason="Emergency", graceful=False)
|
|
110
|
+
"""
|
|
111
|
+
if not self.model:
|
|
112
|
+
logger.error(f"Cannot stop {self.client_id}: model not provided")
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
return await stop.stop_client(
|
|
116
|
+
client=self,
|
|
117
|
+
model=self.model,
|
|
118
|
+
reason=reason,
|
|
119
|
+
graceful=graceful
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
async def update_config(self, fields: Optional[List[str]] = None) -> bool:
|
|
123
|
+
"""
|
|
124
|
+
Push config update to client.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
fields: If specified, only update these fields (partial update)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
True if command sent successfully
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
# Full config update
|
|
134
|
+
success = await client.update_config()
|
|
135
|
+
|
|
136
|
+
# Partial update
|
|
137
|
+
success = await client.update_config(fields=['timeout', 'max_retries'])
|
|
138
|
+
"""
|
|
139
|
+
if not self.model:
|
|
140
|
+
logger.error(f"Cannot update config for {self.client_id}: model not provided")
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
return await config.update_config(
|
|
144
|
+
client=self,
|
|
145
|
+
model=self.model,
|
|
146
|
+
fields=fields
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def restart(
|
|
150
|
+
self,
|
|
151
|
+
reason: Optional[str] = None,
|
|
152
|
+
graceful: bool = True
|
|
153
|
+
) -> bool:
|
|
154
|
+
"""
|
|
155
|
+
Restart the client (stop then start).
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
reason: Optional reason for restart
|
|
159
|
+
graceful: If True, allow graceful stop
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if both stop and start succeeded
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
success = await client.restart(reason="Config changed")
|
|
166
|
+
"""
|
|
167
|
+
# Stop
|
|
168
|
+
stop_success = await self.stop(
|
|
169
|
+
reason=reason or "Restart",
|
|
170
|
+
graceful=graceful
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if not stop_success:
|
|
174
|
+
logger.warning(f"Restart of {self.client_id} failed at stop phase")
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
# Wait a moment for client to stop
|
|
178
|
+
import asyncio
|
|
179
|
+
await asyncio.sleep(1)
|
|
180
|
+
|
|
181
|
+
# Start
|
|
182
|
+
start_success = await self.start(
|
|
183
|
+
reason=reason or "Restart"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if start_success:
|
|
187
|
+
logger.info(f"✅ Restart of {self.client_id} complete")
|
|
188
|
+
else:
|
|
189
|
+
logger.error(f"❌ Restart of {self.client_id} failed at start phase")
|
|
190
|
+
|
|
191
|
+
return start_success
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ============================================================================
|
|
195
|
+
# Batch Operations
|
|
196
|
+
# ============================================================================
|
|
197
|
+
|
|
198
|
+
async def batch_start(
|
|
199
|
+
clients: List[CommandClient],
|
|
200
|
+
reason: Optional[str] = None
|
|
201
|
+
) -> dict:
|
|
202
|
+
"""
|
|
203
|
+
Start multiple clients in parallel.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
clients: List of CommandClient instances
|
|
207
|
+
reason: Optional reason for starting
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Dict with 'success': count, 'failed': count
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
from your_app.grpc.services.commands.registry import get_streaming_service
|
|
214
|
+
|
|
215
|
+
service = get_streaming_service("bots")
|
|
216
|
+
client_ids = ["bot-1", "bot-2", "bot-3"]
|
|
217
|
+
|
|
218
|
+
clients = [
|
|
219
|
+
CommandClient(client_id=cid, model=await Bot.objects.aget(id=cid), streaming_service=service)
|
|
220
|
+
for cid in client_ids
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
results = await batch_start(clients, reason="Batch start")
|
|
224
|
+
print(f"Started {results['success']}/{len(clients)} clients")
|
|
225
|
+
"""
|
|
226
|
+
import asyncio
|
|
227
|
+
|
|
228
|
+
tasks = [client.start(reason=reason) for client in clients]
|
|
229
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
230
|
+
|
|
231
|
+
success_count = sum(1 for r in results if r is True)
|
|
232
|
+
failed_count = len(results) - success_count
|
|
233
|
+
|
|
234
|
+
logger.info(f"Batch start: {success_count} succeeded, {failed_count} failed")
|
|
235
|
+
|
|
236
|
+
return {'success': success_count, 'failed': failed_count}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def batch_stop(
|
|
240
|
+
clients: List[CommandClient],
|
|
241
|
+
reason: Optional[str] = None,
|
|
242
|
+
graceful: bool = True
|
|
243
|
+
) -> dict:
|
|
244
|
+
"""
|
|
245
|
+
Stop multiple clients in parallel.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
clients: List of CommandClient instances
|
|
249
|
+
reason: Optional reason for stopping
|
|
250
|
+
graceful: If True, allow graceful shutdown
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Dict with 'success': count, 'failed': count
|
|
254
|
+
"""
|
|
255
|
+
import asyncio
|
|
256
|
+
|
|
257
|
+
tasks = [client.stop(reason=reason, graceful=graceful) for client in clients]
|
|
258
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
259
|
+
|
|
260
|
+
success_count = sum(1 for r in results if r is True)
|
|
261
|
+
failed_count = len(results) - success_count
|
|
262
|
+
|
|
263
|
+
logger.info(f"Batch stop: {success_count} succeeded, {failed_count} failed")
|
|
264
|
+
|
|
265
|
+
return {'success': success_count, 'failed': failed_count}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
__all__ = [
|
|
269
|
+
'CommandClient',
|
|
270
|
+
'batch_start',
|
|
271
|
+
'batch_stop',
|
|
272
|
+
]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: CONFIG UPDATE Command Implementation
|
|
3
|
+
|
|
4
|
+
Demonstrates pushing configuration updates to connected clients.
|
|
5
|
+
|
|
6
|
+
Key patterns:
|
|
7
|
+
- JSONField updates (Django models)
|
|
8
|
+
- Partial vs full config updates
|
|
9
|
+
- Signal-triggered automatic config sync
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from your_app.grpc.commands.config import update_config
|
|
13
|
+
|
|
14
|
+
# Full config update
|
|
15
|
+
success = await update_config(client, model=instance)
|
|
16
|
+
|
|
17
|
+
# Partial update
|
|
18
|
+
success = await update_config(client, model=instance, fields=['timeout', 'max_retries'])
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Any, Optional, List
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def update_config(
|
|
28
|
+
client, # ExampleCommandClient
|
|
29
|
+
model, # HasConfig protocol
|
|
30
|
+
fields: Optional[List[str]] = None,
|
|
31
|
+
) -> bool:
|
|
32
|
+
"""
|
|
33
|
+
Send CONFIG_UPDATE command to client.
|
|
34
|
+
|
|
35
|
+
Pushes configuration from Django model to connected client.
|
|
36
|
+
Useful for:
|
|
37
|
+
- User updates config in admin/API
|
|
38
|
+
- Django signal automatically pushes to client
|
|
39
|
+
- Batch config updates across multiple clients
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
client: Command client instance
|
|
43
|
+
model: Django model with config JSONField
|
|
44
|
+
fields: If specified, only send these config fields (partial update)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True if command sent successfully, False otherwise
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
# Auto-push on model save (using Django signals)
|
|
51
|
+
from django.db.models.signals import post_save
|
|
52
|
+
from django.dispatch import receiver
|
|
53
|
+
from asgiref.sync import async_to_sync
|
|
54
|
+
|
|
55
|
+
@receiver(post_save, sender=YourModel)
|
|
56
|
+
def on_config_changed(sender, instance, **kwargs):
|
|
57
|
+
from your_app.grpc.commands.config import update_config
|
|
58
|
+
from your_app.grpc.commands.base_client import YourCommandClient
|
|
59
|
+
|
|
60
|
+
client = YourCommandClient(
|
|
61
|
+
client_id=str(instance.id),
|
|
62
|
+
model=instance,
|
|
63
|
+
grpc_port=50051
|
|
64
|
+
)
|
|
65
|
+
async_to_sync(update_config)(client, model=instance)
|
|
66
|
+
|
|
67
|
+
# Manual update from API
|
|
68
|
+
async def update_config_view(request, pk):
|
|
69
|
+
instance = await YourModel.objects.aget(pk=pk)
|
|
70
|
+
# ... update instance.config ...
|
|
71
|
+
await instance.asave(update_fields=['config'])
|
|
72
|
+
|
|
73
|
+
client = YourCommandClient(client_id=str(pk), model=instance, grpc_port=50051)
|
|
74
|
+
success = await update_config(client, model=instance)
|
|
75
|
+
return JsonResponse({"success": success})
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
# Get config from model
|
|
79
|
+
config = model.config
|
|
80
|
+
|
|
81
|
+
# If fields specified, create partial config
|
|
82
|
+
if fields:
|
|
83
|
+
config = {k: v for k, v in config.items() if k in fields}
|
|
84
|
+
logger.debug(f"Sending partial config update to {client.client_id}: {list(config.keys())}")
|
|
85
|
+
else:
|
|
86
|
+
logger.debug(f"Sending full config update to {client.client_id}")
|
|
87
|
+
|
|
88
|
+
# Create protobuf command
|
|
89
|
+
# Example:
|
|
90
|
+
# command = pb2.Command(
|
|
91
|
+
# config_update=pb2.ConfigUpdateCommand(
|
|
92
|
+
# config=json.dumps(config),
|
|
93
|
+
# partial=fields is not None,
|
|
94
|
+
# fields=fields or [],
|
|
95
|
+
# timestamp=int(datetime.now().timestamp())
|
|
96
|
+
# )
|
|
97
|
+
# )
|
|
98
|
+
|
|
99
|
+
logger.warning(
|
|
100
|
+
f"Example implementation: Create your CONFIG_UPDATE command protobuf message here."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Send command
|
|
104
|
+
# success = await client._send_command(command)
|
|
105
|
+
|
|
106
|
+
# For this example:
|
|
107
|
+
# if success:
|
|
108
|
+
# logger.info(f"✅ CONFIG_UPDATE sent to {client.client_id}")
|
|
109
|
+
# else:
|
|
110
|
+
# logger.warning(f"⚠️ Client {client.client_id} not connected, config will sync on reconnect")
|
|
111
|
+
|
|
112
|
+
# return success
|
|
113
|
+
|
|
114
|
+
return False # Placeholder
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(
|
|
118
|
+
f"❌ Error sending CONFIG_UPDATE to {client.client_id}: {e}",
|
|
119
|
+
exc_info=True
|
|
120
|
+
)
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def batch_update_config(
|
|
125
|
+
clients: List[Any], # List[ExampleCommandClient]
|
|
126
|
+
model: Any, # HasConfig
|
|
127
|
+
fields: Optional[List[str]] = None,
|
|
128
|
+
) -> dict:
|
|
129
|
+
"""
|
|
130
|
+
Send config update to multiple clients.
|
|
131
|
+
|
|
132
|
+
Useful for:
|
|
133
|
+
- Updating config across all connected clients of same type
|
|
134
|
+
- Rolling out global config changes
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
clients: List of command client instances
|
|
138
|
+
model: Django model with config
|
|
139
|
+
fields: Optional list of fields for partial update
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dict with 'success': count, 'failed': count
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
from your_app.grpc.services.commands.registry import get_streaming_service
|
|
146
|
+
|
|
147
|
+
# Get all connected clients
|
|
148
|
+
service = get_streaming_service("your_service")
|
|
149
|
+
client_ids = list(service.get_active_connections().keys())
|
|
150
|
+
|
|
151
|
+
# Create clients
|
|
152
|
+
clients = [
|
|
153
|
+
YourCommandClient(client_id=cid, streaming_service=service)
|
|
154
|
+
for cid in client_ids
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
# Batch update
|
|
158
|
+
results = await batch_update_config(clients, global_config_model, fields=['timeout'])
|
|
159
|
+
print(f"Updated {results['success']} clients, {results['failed']} failed")
|
|
160
|
+
"""
|
|
161
|
+
results = {'success': 0, 'failed': 0}
|
|
162
|
+
|
|
163
|
+
for client in clients:
|
|
164
|
+
success = await update_config(client, model, fields)
|
|
165
|
+
if success:
|
|
166
|
+
results['success'] += 1
|
|
167
|
+
else:
|
|
168
|
+
results['failed'] += 1
|
|
169
|
+
|
|
170
|
+
logger.info(
|
|
171
|
+
f"Batch config update complete: {results['success']} succeeded, {results['failed']} failed"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return results
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = ['update_config', 'batch_update_config']
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: START Command Implementation
|
|
3
|
+
|
|
4
|
+
This demonstrates the command decomposition pattern where each command
|
|
5
|
+
is implemented as a standalone async function.
|
|
6
|
+
|
|
7
|
+
Key patterns:
|
|
8
|
+
- Type-safe model operations using Protocol
|
|
9
|
+
- Django async ORM (asave with update_fields)
|
|
10
|
+
- Protobuf command creation
|
|
11
|
+
- Error handling and logging
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
from your_app.grpc.commands.start import start_client
|
|
15
|
+
from your_app.grpc.commands.base_client import YourCommandClient
|
|
16
|
+
|
|
17
|
+
# Create client
|
|
18
|
+
client = YourCommandClient(client_id="123", model=instance)
|
|
19
|
+
|
|
20
|
+
# Call command function
|
|
21
|
+
success = await start_client(client, model=instance, reason="User request")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def start_client(
|
|
31
|
+
client, # ExampleCommandClient
|
|
32
|
+
model, # HasStatus protocol
|
|
33
|
+
reason: Optional[str] = None,
|
|
34
|
+
) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Send START command to client.
|
|
37
|
+
|
|
38
|
+
This is a standalone command function that can be:
|
|
39
|
+
1. Called directly from management commands
|
|
40
|
+
2. Wrapped in a method on the client class
|
|
41
|
+
3. Used in REST API views
|
|
42
|
+
4. Triggered by Django signals
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
client: Command client instance
|
|
46
|
+
model: Django model with status field (HasStatus protocol)
|
|
47
|
+
reason: Optional reason for starting
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if command sent successfully, False otherwise
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
# From REST API
|
|
54
|
+
from your_app.grpc.commands.start import start_client
|
|
55
|
+
from your_app.grpc.commands.base_client import YourCommandClient
|
|
56
|
+
|
|
57
|
+
async def start_view(request, pk):
|
|
58
|
+
instance = await YourModel.objects.aget(pk=pk)
|
|
59
|
+
client = YourCommandClient(client_id=str(pk), model=instance, grpc_port=50051)
|
|
60
|
+
success = await start_client(client, model=instance, reason="API request")
|
|
61
|
+
return JsonResponse({"success": success})
|
|
62
|
+
|
|
63
|
+
# From management command (same-process)
|
|
64
|
+
from your_app.grpc.services.registry import get_streaming_service
|
|
65
|
+
|
|
66
|
+
service = get_streaming_service("your_service")
|
|
67
|
+
client = YourCommandClient(client_id=str(pk), model=instance, streaming_service=service)
|
|
68
|
+
success = await start_client(client, model=instance)
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
# Update model status to STARTING
|
|
72
|
+
model.status = "STARTING"
|
|
73
|
+
await model.asave(update_fields=['status'])
|
|
74
|
+
logger.info(f"Updated {client.client_id} status to STARTING")
|
|
75
|
+
|
|
76
|
+
# Create protobuf command
|
|
77
|
+
# IMPORTANT: Replace with your actual protobuf types
|
|
78
|
+
# Example:
|
|
79
|
+
# command = pb2.Command(
|
|
80
|
+
# start=pb2.StartCommand(
|
|
81
|
+
# reason=reason or "Manual start",
|
|
82
|
+
# timestamp=int(datetime.now().timestamp())
|
|
83
|
+
# )
|
|
84
|
+
# )
|
|
85
|
+
|
|
86
|
+
# For this example, we'll just log a warning
|
|
87
|
+
logger.warning(
|
|
88
|
+
f"Example implementation: Create your START command protobuf message here. "
|
|
89
|
+
f"See trading_bots/grpc/services/commands/start.py for reference."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Send command via client (auto-detects same-process vs cross-process)
|
|
93
|
+
# success = await client._send_command(command)
|
|
94
|
+
|
|
95
|
+
# For this example, return placeholder
|
|
96
|
+
# if success:
|
|
97
|
+
# logger.info(f"✅ START command sent to {client.client_id}")
|
|
98
|
+
# # Optionally update status to RUNNING
|
|
99
|
+
# model.status = "RUNNING"
|
|
100
|
+
# await model.asave(update_fields=['status'])
|
|
101
|
+
# else:
|
|
102
|
+
# logger.warning(f"⚠️ Client {client.client_id} not connected")
|
|
103
|
+
# # Revert status
|
|
104
|
+
# model.status = "STOPPED"
|
|
105
|
+
# await model.asave(update_fields=['status'])
|
|
106
|
+
|
|
107
|
+
# return success
|
|
108
|
+
|
|
109
|
+
return False # Placeholder
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(
|
|
113
|
+
f"❌ Error sending START command to {client.client_id}: {e}",
|
|
114
|
+
exc_info=True
|
|
115
|
+
)
|
|
116
|
+
# Revert model status on error
|
|
117
|
+
try:
|
|
118
|
+
model.status = "ERROR"
|
|
119
|
+
await model.asave(update_fields=['status'])
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = ['start_client']
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: STOP Command Implementation
|
|
3
|
+
|
|
4
|
+
Demonstrates stopping a client with graceful shutdown and status updates.
|
|
5
|
+
|
|
6
|
+
Key patterns:
|
|
7
|
+
- Status transition (RUNNING → STOPPING → STOPPED)
|
|
8
|
+
- Optional reason tracking
|
|
9
|
+
- Error recovery
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from your_app.grpc.commands.stop import stop_client
|
|
13
|
+
|
|
14
|
+
success = await stop_client(client, model=instance, reason="Maintenance")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def stop_client(
|
|
24
|
+
client, # ExampleCommandClient
|
|
25
|
+
model, # HasStatus protocol
|
|
26
|
+
reason: Optional[str] = None,
|
|
27
|
+
graceful: bool = True,
|
|
28
|
+
) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Send STOP command to client.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
client: Command client instance
|
|
34
|
+
model: Django model with status field
|
|
35
|
+
reason: Optional reason for stopping
|
|
36
|
+
graceful: If True, allow client to finish current work
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
True if command sent successfully, False otherwise
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
# Graceful stop
|
|
43
|
+
success = await stop_client(client, model, reason="Scheduled maintenance", graceful=True)
|
|
44
|
+
|
|
45
|
+
# Force stop
|
|
46
|
+
success = await stop_client(client, model, reason="Emergency", graceful=False)
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
# Update model status
|
|
50
|
+
current_status = model.status
|
|
51
|
+
model.status = "STOPPING"
|
|
52
|
+
await model.asave(update_fields=['status'])
|
|
53
|
+
logger.info(f"Updated {client.client_id} status to STOPPING")
|
|
54
|
+
|
|
55
|
+
# Create protobuf command
|
|
56
|
+
# Example:
|
|
57
|
+
# command = pb2.Command(
|
|
58
|
+
# stop=pb2.StopCommand(
|
|
59
|
+
# reason=reason or "Manual stop",
|
|
60
|
+
# graceful=graceful,
|
|
61
|
+
# timestamp=int(datetime.now().timestamp())
|
|
62
|
+
# )
|
|
63
|
+
# )
|
|
64
|
+
|
|
65
|
+
logger.warning(
|
|
66
|
+
f"Example implementation: Create your STOP command protobuf message here."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Send command
|
|
70
|
+
# success = await client._send_command(command)
|
|
71
|
+
|
|
72
|
+
# For this example:
|
|
73
|
+
# if success:
|
|
74
|
+
# logger.info(f"✅ STOP command sent to {client.client_id}")
|
|
75
|
+
# model.status = "STOPPED"
|
|
76
|
+
# await model.asave(update_fields=['status'])
|
|
77
|
+
# else:
|
|
78
|
+
# logger.warning(f"⚠️ Client {client.client_id} not connected")
|
|
79
|
+
# # Revert to previous status
|
|
80
|
+
# model.status = current_status
|
|
81
|
+
# await model.asave(update_fields=['status'])
|
|
82
|
+
|
|
83
|
+
# return success
|
|
84
|
+
|
|
85
|
+
return False # Placeholder
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.error(
|
|
89
|
+
f"❌ Error sending STOP command to {client.client_id}: {e}",
|
|
90
|
+
exc_info=True
|
|
91
|
+
)
|
|
92
|
+
# Handle error
|
|
93
|
+
try:
|
|
94
|
+
model.status = "ERROR"
|
|
95
|
+
await model.asave(update_fields=['status'])
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
__all__ = ['stop_client']
|