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.

Files changed (98) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
  3. django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
  4. django_cfg/apps/integrations/centrifugo/services/logging.py +90 -14
  5. django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
  6. django_cfg/apps/integrations/centrifugo/views/testing_api.py +47 -43
  7. django_cfg/apps/integrations/centrifugo/views/wrapper.py +41 -29
  8. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
  9. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +1 -1
  10. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +22 -36
  11. django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
  12. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
  13. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
  14. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
  15. django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
  16. django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
  17. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/demo.py +1 -1
  18. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/test_publish.py +4 -4
  19. django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
  20. django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
  21. django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
  22. django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
  23. django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
  24. django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
  25. django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
  26. django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
  27. django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
  28. django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
  29. django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
  30. django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
  31. django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
  32. django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +62 -55
  33. django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +216 -5
  34. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
  35. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
  36. django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
  37. django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
  38. django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
  39. django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
  40. django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
  41. django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
  42. django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
  43. django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
  44. django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
  45. django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
  46. django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
  47. django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
  48. django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
  49. django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
  50. django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
  51. django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
  52. django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
  53. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +55 -8
  54. django_cfg/apps/integrations/grpc/views/charts.py +1 -1
  55. django_cfg/apps/integrations/grpc/views/config.py +1 -1
  56. django_cfg/core/base/config_model.py +11 -0
  57. django_cfg/core/builders/middleware_builder.py +5 -0
  58. django_cfg/management/commands/pool_status.py +153 -0
  59. django_cfg/middleware/pool_cleanup.py +261 -0
  60. django_cfg/models/api/grpc/config.py +2 -2
  61. django_cfg/models/infrastructure/database/config.py +16 -0
  62. django_cfg/models/infrastructure/database/converters.py +2 -0
  63. django_cfg/modules/django_admin/utils/html/composition.py +57 -13
  64. django_cfg/modules/django_admin/utils/html_builder.py +1 -0
  65. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +12 -0
  66. django_cfg/modules/django_client/core/generator/typescript/generator.py +8 -0
  67. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +22 -0
  68. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +4 -0
  69. django_cfg/modules/django_client/core/generator/typescript/templates/utils/validation-events.ts.jinja +133 -0
  70. django_cfg/modules/django_client/core/groups/manager.py +25 -18
  71. django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
  72. django_cfg/modules/django_client/urls.py +38 -5
  73. django_cfg/modules/django_logging/django_logger.py +58 -19
  74. django_cfg/modules/django_twilio/email_otp.py +3 -1
  75. django_cfg/modules/django_twilio/sms.py +3 -1
  76. django_cfg/modules/django_twilio/unified.py +6 -2
  77. django_cfg/modules/django_twilio/whatsapp.py +3 -1
  78. django_cfg/pyproject.toml +3 -3
  79. django_cfg/static/frontend/admin.zip +0 -0
  80. django_cfg/templates/admin/index.html +17 -57
  81. django_cfg/utils/pool_monitor.py +320 -0
  82. django_cfg/utils/smart_defaults.py +233 -7
  83. {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/METADATA +75 -5
  84. {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/RECORD +97 -68
  85. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
  86. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
  87. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
  88. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
  89. /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
  90. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
  91. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
  92. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
  93. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
  94. /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
  95. /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
  96. {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/WHEEL +0 -0
  97. {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/entry_points.txt +0 -0
  98. {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']