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,541 @@
1
+ """
2
+ Centrifugo Publishing Interceptor for gRPC.
3
+
4
+ Automatically publishes gRPC call metadata to Centrifugo WebSocket channels.
5
+ Works alongside CentrifugoBridgeMixin for complete event visibility.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import time
12
+ from datetime import datetime, timezone as tz
13
+ from typing import Callable, Optional, Any, Dict
14
+
15
+ import grpc
16
+ import grpc.aio
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class CentrifugoInterceptor(grpc.aio.ServerInterceptor):
22
+ """
23
+ Async gRPC interceptor that publishes call metadata to Centrifugo.
24
+
25
+ Automatically publishes:
26
+ - RPC method invocations (start/end)
27
+ - Timing information
28
+ - Status codes
29
+ - Message counts
30
+ - Error information
31
+ - Client peer information
32
+
33
+ Works in parallel with CentrifugoBridgeMixin:
34
+ - Interceptor: Publishes RPC-level metadata (method, timing, status)
35
+ - Mixin: Publishes message-level data (protobuf field contents)
36
+
37
+ Example:
38
+ ```python
39
+ # In Django settings
40
+ GRPC_FRAMEWORK = {
41
+ "SERVER_INTERCEPTORS": [
42
+ "django_cfg.apps.integrations.grpc.interceptors.CentrifugoInterceptor",
43
+ ]
44
+ }
45
+ ```
46
+
47
+ Channel naming:
48
+ - RPC calls: `grpc#{service}#{method}#meta`
49
+ - Errors: `grpc#{service}#{method}#errors`
50
+
51
+ Published metadata:
52
+ {
53
+ "event_type": "rpc_start" | "rpc_end" | "rpc_error",
54
+ "method": "/service.Service/Method",
55
+ "service": "service.Service",
56
+ "method_name": "Method",
57
+ "peer": "ipv4:127.0.0.1:12345",
58
+ "timestamp": "2025-11-05T...",
59
+ "duration_ms": 123.45, # Only on rpc_end
60
+ "status": "OK" | "ERROR",
61
+ "message_count": 10, # For streaming
62
+ "error": {...}, # Only on error
63
+ }
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ enabled: bool = True,
69
+ publish_start: bool = False,
70
+ publish_end: bool = True,
71
+ publish_errors: bool = True,
72
+ publish_stream_messages: bool = False,
73
+ channel_template: str = "grpc#{service}#{method}#meta",
74
+ error_channel_template: str = "grpc#{service}#{method}#errors",
75
+ metadata: Optional[Dict[str, Any]] = None,
76
+ ):
77
+ """
78
+ Initialize Centrifugo interceptor.
79
+
80
+ Args:
81
+ enabled: Enable/disable publishing
82
+ publish_start: Publish RPC start events
83
+ publish_end: Publish RPC completion events
84
+ publish_errors: Publish RPC error events
85
+ publish_stream_messages: Publish each streaming message (can be noisy)
86
+ channel_template: Channel name template for metadata
87
+ error_channel_template: Channel name template for errors
88
+ metadata: Additional metadata to include in all events
89
+ """
90
+ self.enabled = enabled
91
+ self.publish_start = publish_start
92
+ self.publish_end = publish_end
93
+ self.publish_errors = publish_errors
94
+ self.publish_stream_messages = publish_stream_messages
95
+ self.channel_template = channel_template
96
+ self.error_channel_template = error_channel_template
97
+ self.metadata = metadata or {}
98
+
99
+ self._centrifugo_publisher: Optional[Any] = None
100
+ self._initialize_publisher()
101
+
102
+ def _initialize_publisher(self):
103
+ """Initialize Centrifugo publisher lazily with direct client."""
104
+ if not self.enabled:
105
+ logger.debug("CentrifugoInterceptor disabled")
106
+ return
107
+
108
+ try:
109
+ from django_cfg.apps.integrations.centrifugo.services import get_centrifugo_publisher
110
+ # Use Publisher with DirectClient (use_direct=True by default)
111
+ # This bypasses wrapper and goes directly to Centrifugo
112
+ self._centrifugo_publisher = get_centrifugo_publisher()
113
+ logger.info("CentrifugoInterceptor initialized with DirectCentrifugoClient")
114
+ except Exception as e:
115
+ logger.warning(
116
+ f"Failed to initialize Centrifugo publisher in interceptor: {e}. "
117
+ f"Interceptor will continue without publishing."
118
+ )
119
+ self.enabled = False
120
+
121
+ async def intercept_service(
122
+ self,
123
+ continuation: Callable,
124
+ handler_call_details: grpc.HandlerCallDetails,
125
+ ) -> grpc.RpcMethodHandler:
126
+ """
127
+ Intercept async gRPC service call for Centrifugo publishing.
128
+
129
+ Args:
130
+ continuation: Function to invoke the next interceptor or handler
131
+ handler_call_details: Details about the RPC call
132
+
133
+ Returns:
134
+ RPC method handler with Centrifugo publishing
135
+ """
136
+ if not self.enabled or not self._centrifugo_publisher:
137
+ # Pass through without interception
138
+ return await continuation(handler_call_details)
139
+
140
+ method_name = handler_call_details.method
141
+ peer = self._extract_peer(handler_call_details.invocation_metadata)
142
+ service_name, method_short = self._parse_method_name(method_name)
143
+
144
+ # Publish start event
145
+ if self.publish_start:
146
+ await self._publish_event(
147
+ event_type="rpc_start",
148
+ method=method_name,
149
+ service=service_name,
150
+ method_name=method_short,
151
+ peer=peer,
152
+ )
153
+
154
+ # Get handler and wrap it
155
+ handler = await continuation(handler_call_details)
156
+
157
+ if handler is None:
158
+ logger.warning(f"[CentrifugoInterceptor] No handler found for {method_name}")
159
+ return None
160
+
161
+ # Wrap handler methods to publish events
162
+ return self._wrap_handler(handler, method_name, service_name, method_short, peer)
163
+
164
+ def _wrap_handler(
165
+ self,
166
+ handler: grpc.RpcMethodHandler,
167
+ method_name: str,
168
+ service_name: str,
169
+ method_short: str,
170
+ peer: str,
171
+ ) -> grpc.RpcMethodHandler:
172
+ """
173
+ Wrap handler to add Centrifugo publishing.
174
+
175
+ Args:
176
+ handler: Original RPC method handler
177
+ method_name: Full gRPC method name
178
+ service_name: Service name
179
+ method_short: Short method name
180
+ peer: Client peer information
181
+
182
+ Returns:
183
+ Wrapped RPC method handler
184
+ """
185
+ # Determine handler type and wrap accordingly
186
+ if handler.unary_unary:
187
+ wrapped = self._wrap_unary_unary(
188
+ handler.unary_unary, method_name, service_name, method_short, peer
189
+ )
190
+ return grpc.unary_unary_rpc_method_handler(
191
+ wrapped,
192
+ request_deserializer=handler.request_deserializer,
193
+ response_serializer=handler.response_serializer,
194
+ )
195
+
196
+ if handler.unary_stream:
197
+ wrapped = self._wrap_unary_stream(
198
+ handler.unary_stream, method_name, service_name, method_short, peer
199
+ )
200
+ return grpc.unary_stream_rpc_method_handler(
201
+ wrapped,
202
+ request_deserializer=handler.request_deserializer,
203
+ response_serializer=handler.response_serializer,
204
+ )
205
+
206
+ if handler.stream_unary:
207
+ wrapped = self._wrap_stream_unary(
208
+ handler.stream_unary, method_name, service_name, method_short, peer
209
+ )
210
+ return grpc.stream_unary_rpc_method_handler(
211
+ wrapped,
212
+ request_deserializer=handler.request_deserializer,
213
+ response_serializer=handler.response_serializer,
214
+ )
215
+
216
+ if handler.stream_stream:
217
+ wrapped = self._wrap_stream_stream(
218
+ handler.stream_stream, method_name, service_name, method_short, peer
219
+ )
220
+ return grpc.stream_stream_rpc_method_handler(
221
+ wrapped,
222
+ request_deserializer=handler.request_deserializer,
223
+ response_serializer=handler.response_serializer,
224
+ )
225
+
226
+ return handler
227
+
228
+ def _wrap_unary_unary(self, behavior, method_name, service_name, method_short, peer):
229
+ """Wrap unary-unary RPC."""
230
+ async def wrapper(request, context):
231
+ start_time = time.time()
232
+ try:
233
+ response = await behavior(request, context)
234
+ duration = (time.time() - start_time) * 1000
235
+
236
+ if self.publish_end:
237
+ await self._publish_event(
238
+ event_type="rpc_end",
239
+ method=method_name,
240
+ service=service_name,
241
+ method_name=method_short,
242
+ peer=peer,
243
+ duration_ms=duration,
244
+ status="OK",
245
+ )
246
+
247
+ return response
248
+ except Exception as e:
249
+ duration = (time.time() - start_time) * 1000
250
+
251
+ if self.publish_errors:
252
+ await self._publish_error(
253
+ method=method_name,
254
+ service=service_name,
255
+ method_name=method_short,
256
+ peer=peer,
257
+ duration_ms=duration,
258
+ error=e,
259
+ )
260
+ raise
261
+
262
+ return wrapper
263
+
264
+ def _wrap_unary_stream(self, behavior, method_name, service_name, method_short, peer):
265
+ """Wrap unary-stream RPC."""
266
+ async def wrapper(request, context):
267
+ start_time = time.time()
268
+ message_count = 0
269
+ try:
270
+ async for response in behavior(request, context):
271
+ message_count += 1
272
+
273
+ if self.publish_stream_messages:
274
+ await self._publish_event(
275
+ event_type="stream_message",
276
+ method=method_name,
277
+ service=service_name,
278
+ method_name=method_short,
279
+ peer=peer,
280
+ message_count=message_count,
281
+ direction="server_to_client",
282
+ )
283
+
284
+ yield response
285
+
286
+ duration = (time.time() - start_time) * 1000
287
+
288
+ if self.publish_end:
289
+ await self._publish_event(
290
+ event_type="rpc_end",
291
+ method=method_name,
292
+ service=service_name,
293
+ method_name=method_short,
294
+ peer=peer,
295
+ duration_ms=duration,
296
+ status="OK",
297
+ message_count=message_count,
298
+ )
299
+
300
+ except Exception as e:
301
+ duration = (time.time() - start_time) * 1000
302
+
303
+ if self.publish_errors:
304
+ await self._publish_error(
305
+ method=method_name,
306
+ service=service_name,
307
+ method_name=method_short,
308
+ peer=peer,
309
+ duration_ms=duration,
310
+ error=e,
311
+ message_count=message_count,
312
+ )
313
+ raise
314
+
315
+ return wrapper
316
+
317
+ def _wrap_stream_unary(self, behavior, method_name, service_name, method_short, peer):
318
+ """Wrap stream-unary RPC."""
319
+ async def wrapper(request_iterator, context):
320
+ start_time = time.time()
321
+ message_count = 0
322
+ try:
323
+ # Count incoming messages
324
+ requests = []
325
+ async for req in request_iterator:
326
+ message_count += 1
327
+
328
+ if self.publish_stream_messages:
329
+ await self._publish_event(
330
+ event_type="stream_message",
331
+ method=method_name,
332
+ service=service_name,
333
+ method_name=method_short,
334
+ peer=peer,
335
+ message_count=message_count,
336
+ direction="client_to_server",
337
+ )
338
+
339
+ requests.append(req)
340
+
341
+ # Process
342
+ async def request_iter():
343
+ for r in requests:
344
+ yield r
345
+
346
+ response = await behavior(request_iter(), context)
347
+ duration = (time.time() - start_time) * 1000
348
+
349
+ if self.publish_end:
350
+ await self._publish_event(
351
+ event_type="rpc_end",
352
+ method=method_name,
353
+ service=service_name,
354
+ method_name=method_short,
355
+ peer=peer,
356
+ duration_ms=duration,
357
+ status="OK",
358
+ message_count=message_count,
359
+ )
360
+
361
+ return response
362
+ except Exception as e:
363
+ duration = (time.time() - start_time) * 1000
364
+
365
+ if self.publish_errors:
366
+ await self._publish_error(
367
+ method=method_name,
368
+ service=service_name,
369
+ method_name=method_short,
370
+ peer=peer,
371
+ duration_ms=duration,
372
+ error=e,
373
+ message_count=message_count,
374
+ )
375
+ raise
376
+
377
+ return wrapper
378
+
379
+ def _wrap_stream_stream(self, behavior, method_name, service_name, method_short, peer):
380
+ """Wrap bidirectional streaming RPC."""
381
+ async def wrapper(request_iterator, context):
382
+ start_time = time.time()
383
+ in_count = 0
384
+ out_count = 0
385
+ try:
386
+ # Wrap request iterator to count messages
387
+ async def counting_iterator():
388
+ nonlocal in_count
389
+ async for req in request_iterator:
390
+ in_count += 1
391
+
392
+ if self.publish_stream_messages:
393
+ await self._publish_event(
394
+ event_type="stream_message",
395
+ method=method_name,
396
+ service=service_name,
397
+ method_name=method_short,
398
+ peer=peer,
399
+ message_count=in_count,
400
+ direction="client_to_server",
401
+ )
402
+
403
+ yield req
404
+
405
+ # Stream responses
406
+ async for response in behavior(counting_iterator(), context):
407
+ out_count += 1
408
+
409
+ if self.publish_stream_messages:
410
+ await self._publish_event(
411
+ event_type="stream_message",
412
+ method=method_name,
413
+ service=service_name,
414
+ method_name=method_short,
415
+ peer=peer,
416
+ message_count=out_count,
417
+ direction="server_to_client",
418
+ )
419
+
420
+ yield response
421
+
422
+ duration = (time.time() - start_time) * 1000
423
+
424
+ if self.publish_end:
425
+ await self._publish_event(
426
+ event_type="rpc_end",
427
+ method=method_name,
428
+ service=service_name,
429
+ method_name=method_short,
430
+ peer=peer,
431
+ duration_ms=duration,
432
+ status="OK",
433
+ in_message_count=in_count,
434
+ out_message_count=out_count,
435
+ )
436
+
437
+ except Exception as e:
438
+ duration = (time.time() - start_time) * 1000
439
+
440
+ if self.publish_errors:
441
+ await self._publish_error(
442
+ method=method_name,
443
+ service=service_name,
444
+ method_name=method_short,
445
+ peer=peer,
446
+ duration_ms=duration,
447
+ error=e,
448
+ in_message_count=in_count,
449
+ out_message_count=out_count,
450
+ )
451
+ raise
452
+
453
+ return wrapper
454
+
455
+ async def _publish_event(self, **data):
456
+ """Publish event to Centrifugo via Publisher."""
457
+ try:
458
+ # Build channel name
459
+ channel = self.channel_template.format(
460
+ service=data.get('service', 'unknown'),
461
+ method=data.get('method_name', 'unknown'),
462
+ )
463
+
464
+ # Use Publisher's publish_grpc_event for type-safe gRPC events
465
+ await self._centrifugo_publisher.publish_grpc_event(
466
+ channel=channel,
467
+ method=data.get('method', ''),
468
+ status=data.get('status', 'UNKNOWN'),
469
+ duration_ms=data.get('duration_ms', 0.0),
470
+ peer=data.get('peer'),
471
+ metadata={
472
+ 'event_type': data.get('event_type'),
473
+ **self.metadata,
474
+ },
475
+ **{k: v for k, v in data.items() if k not in ['method', 'status', 'duration_ms', 'peer', 'event_type', 'service', 'method_name']},
476
+ )
477
+
478
+ logger.debug(f"Published gRPC event to {channel}: {data.get('event_type')}")
479
+
480
+ except Exception as e:
481
+ logger.warning(f"Failed to publish gRPC event to Centrifugo: {e}")
482
+
483
+ async def _publish_error(self, error: Exception, **data):
484
+ """Publish error to Centrifugo via Publisher."""
485
+ try:
486
+ # Build error channel name
487
+ channel = self.error_channel_template.format(
488
+ service=data.get('service', 'unknown'),
489
+ method=data.get('method_name', 'unknown'),
490
+ )
491
+
492
+ # Use Publisher's publish_grpc_event with error status
493
+ await self._centrifugo_publisher.publish_grpc_event(
494
+ channel=channel,
495
+ method=data.get('method', ''),
496
+ status='ERROR',
497
+ duration_ms=data.get('duration_ms', 0.0),
498
+ peer=data.get('peer'),
499
+ metadata={
500
+ 'event_type': 'rpc_error',
501
+ 'error': {
502
+ 'type': type(error).__name__,
503
+ 'message': str(error),
504
+ },
505
+ **self.metadata,
506
+ },
507
+ **{k: v for k, v in data.items() if k not in ['method', 'duration_ms', 'peer', 'error', 'service', 'method_name']},
508
+ )
509
+
510
+ logger.debug(f"Published gRPC error to {channel}")
511
+
512
+ except Exception as e:
513
+ logger.warning(f"Failed to publish gRPC error to Centrifugo: {e}")
514
+
515
+ @staticmethod
516
+ def _extract_peer(invocation_metadata) -> str:
517
+ """Extract peer information from metadata."""
518
+ if invocation_metadata:
519
+ for key, value in invocation_metadata:
520
+ if key == "x-forwarded-for":
521
+ return value
522
+ return "unknown"
523
+
524
+ @staticmethod
525
+ def _parse_method_name(full_method: str) -> tuple[str, str]:
526
+ """
527
+ Parse full gRPC method name.
528
+
529
+ Args:
530
+ full_method: e.g., "/trading_bots.BotStreamingService/ConnectBot"
531
+
532
+ Returns:
533
+ (service_name, method_name): ("trading_bots.BotStreamingService", "ConnectBot")
534
+ """
535
+ parts = full_method.strip("/").split("/")
536
+ if len(parts) == 2:
537
+ return parts[0], parts[1]
538
+ return "unknown", full_method
539
+
540
+
541
+ __all__ = ["CentrifugoInterceptor"]
@@ -109,7 +109,7 @@ def get_metrics(method: str = None) -> dict:
109
109
 
110
110
  Example:
111
111
  ```python
112
- from django_cfg.apps.integrations.grpc.interceptors.metrics import get_metrics
112
+ from django_cfg.apps.integrations.grpc.services.interceptors.metrics import get_metrics
113
113
 
114
114
  # Get all metrics
115
115
  all_stats = get_metrics()
@@ -129,7 +129,7 @@ def reset_metrics():
129
129
 
130
130
  Example:
131
131
  ```python
132
- from django_cfg.apps.integrations.grpc.interceptors.metrics import reset_metrics
132
+ from django_cfg.apps.integrations.grpc.services.interceptors.metrics import reset_metrics
133
133
  reset_metrics()
134
134
  ```
135
135
  """
@@ -159,7 +159,7 @@ class MetricsInterceptor(grpc.aio.ServerInterceptor):
159
159
 
160
160
  Access Metrics:
161
161
  ```python
162
- from django_cfg.apps.integrations.grpc.interceptors.metrics import get_metrics
162
+ from django_cfg.apps.integrations.grpc.services.interceptors.metrics import get_metrics
163
163
 
164
164
  stats = get_metrics()
165
165
  print(f"Total requests: {stats['total_requests']}")
@@ -367,8 +367,8 @@ class RequestLoggerInterceptor(grpc.aio.ServerInterceptor):
367
367
  ):
368
368
  """Create initial log entry in database (async)."""
369
369
  try:
370
- from ..models import GRPCRequestLog
371
- from ..auth import get_current_grpc_user, get_current_grpc_api_key
370
+ from ...models import GRPCRequestLog
371
+ from ...auth import get_current_grpc_user, get_current_grpc_api_key
372
372
 
373
373
  # Get user and api_key from contextvars (set by ApiKeyAuthInterceptor)
374
374
  user = get_current_grpc_user()
@@ -380,9 +380,8 @@ class RequestLoggerInterceptor(grpc.aio.ServerInterceptor):
380
380
  # Extract client IP from peer
381
381
  client_ip = self._extract_ip_from_peer(peer)
382
382
 
383
- # Create log entry (wrap Django ORM in asyncio.to_thread)
384
- log_entry = await asyncio.to_thread(
385
- GRPCRequestLog.objects.create,
383
+ # Create log entry (Django 5.2: Native async ORM)
384
+ log_entry = await GRPCRequestLog.objects.acreate(
386
385
  request_id=request_id,
387
386
  service_name=service_name,
388
387
  method_name=method_name,
@@ -416,15 +415,14 @@ class RequestLoggerInterceptor(grpc.aio.ServerInterceptor):
416
415
  return
417
416
 
418
417
  try:
419
- from ..models import GRPCRequestLog
418
+ from ...models import GRPCRequestLog
420
419
 
421
420
  # Prepare response data
422
421
  if response:
423
422
  response_data = self._serialize_message(response)
424
423
 
425
- # Wrap Django ORM in asyncio.to_thread
426
- await asyncio.to_thread(
427
- GRPCRequestLog.objects.mark_success,
424
+ # Django 5.2: Use async manager method
425
+ await GRPCRequestLog.objects.amark_success(
428
426
  log_entry,
429
427
  duration_ms=duration_ms,
430
428
  response_data=response_data,
@@ -445,14 +443,13 @@ class RequestLoggerInterceptor(grpc.aio.ServerInterceptor):
445
443
  return
446
444
 
447
445
  try:
448
- from ..models import GRPCRequestLog
446
+ from ...models import GRPCRequestLog
449
447
 
450
448
  # Get gRPC status code
451
449
  grpc_code = self._get_grpc_code(error, context)
452
450
 
453
- # Wrap Django ORM in asyncio.to_thread
454
- await asyncio.to_thread(
455
- GRPCRequestLog.objects.mark_error,
451
+ # Django 5.2: Use async manager method
452
+ await GRPCRequestLog.objects.amark_error(
456
453
  log_entry,
457
454
  grpc_status_code=grpc_code,
458
455
  error_message=str(error),
@@ -0,0 +1,37 @@
1
+ """
2
+ gRPC service management utilities.
3
+
4
+ This package provides tools for managing protobuf files, configuration,
5
+ and service lifecycle.
6
+
7
+ **Components**:
8
+ - proto_manager: Protobuf file management and compilation
9
+ - config_helper: Configuration utilities for gRPC services
10
+
11
+ **Usage Example**:
12
+ ```python
13
+ from django_cfg.apps.integrations.grpc.services.management import (
14
+ ProtoFilesManager,
15
+ ConfigHelper,
16
+ )
17
+
18
+ # Manage proto files
19
+ proto_mgr = ProtoFilesManager()
20
+ proto_mgr.compile_all()
21
+
22
+ # Configuration helpers
23
+ config = ConfigHelper.get_grpc_config()
24
+ ```
25
+
26
+ Created: 2025-11-07
27
+ Status: %%PRODUCTION%%
28
+ """
29
+
30
+ # Export when modules are refactored
31
+ # from .proto_manager import ProtoFilesManager
32
+ # from .config_helper import ConfigHelper
33
+
34
+ __all__ = [
35
+ # 'ProtoFilesManager',
36
+ # 'ConfigHelper',
37
+ ]