django-cfg 1.4.120__py3-none-any.whl → 1.5.1__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 (80) hide show
  1. django_cfg/__init__.py +8 -4
  2. django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
  3. django_cfg/apps/grpc/__init__.py +9 -0
  4. django_cfg/apps/grpc/admin/__init__.py +11 -0
  5. django_cfg/apps/grpc/admin/config.py +89 -0
  6. django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
  7. django_cfg/apps/grpc/apps.py +28 -0
  8. django_cfg/apps/grpc/auth/__init__.py +9 -0
  9. django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
  10. django_cfg/apps/grpc/interceptors/__init__.py +19 -0
  11. django_cfg/apps/grpc/interceptors/errors.py +241 -0
  12. django_cfg/apps/grpc/interceptors/logging.py +270 -0
  13. django_cfg/apps/grpc/interceptors/metrics.py +306 -0
  14. django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
  15. django_cfg/apps/grpc/management/__init__.py +1 -0
  16. django_cfg/apps/grpc/management/commands/__init__.py +0 -0
  17. django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
  18. django_cfg/apps/grpc/managers/__init__.py +10 -0
  19. django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
  20. django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
  21. django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
  22. django_cfg/apps/grpc/migrations/__init__.py +0 -0
  23. django_cfg/apps/grpc/models/__init__.py +9 -0
  24. django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
  25. django_cfg/apps/grpc/serializers/__init__.py +23 -0
  26. django_cfg/apps/grpc/serializers/health.py +18 -0
  27. django_cfg/apps/grpc/serializers/requests.py +18 -0
  28. django_cfg/apps/grpc/serializers/services.py +50 -0
  29. django_cfg/apps/grpc/serializers/stats.py +22 -0
  30. django_cfg/apps/grpc/services/__init__.py +16 -0
  31. django_cfg/apps/grpc/services/base.py +375 -0
  32. django_cfg/apps/grpc/services/discovery.py +415 -0
  33. django_cfg/apps/grpc/urls.py +23 -0
  34. django_cfg/apps/grpc/utils/__init__.py +13 -0
  35. django_cfg/apps/grpc/utils/proto_gen.py +423 -0
  36. django_cfg/apps/grpc/views/__init__.py +9 -0
  37. django_cfg/apps/grpc/views/monitoring.py +497 -0
  38. django_cfg/apps/maintenance/admin/api_key_admin.py +7 -8
  39. django_cfg/apps/maintenance/admin/site_admin.py +5 -4
  40. django_cfg/apps/payments/admin/balance_admin.py +26 -36
  41. django_cfg/apps/payments/admin/payment_admin.py +65 -85
  42. django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
  43. django_cfg/apps/tasks/admin/task_log.py +20 -47
  44. django_cfg/apps/urls.py +7 -1
  45. django_cfg/config.py +106 -0
  46. django_cfg/core/base/config_model.py +6 -0
  47. django_cfg/core/builders/apps_builder.py +3 -0
  48. django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
  49. django_cfg/core/generation/orchestrator.py +10 -0
  50. django_cfg/models/api/grpc/__init__.py +59 -0
  51. django_cfg/models/api/grpc/config.py +364 -0
  52. django_cfg/modules/base.py +15 -0
  53. django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
  54. django_cfg/modules/django_admin/utils/__init__.py +41 -3
  55. django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
  56. django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
  57. django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
  58. django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
  59. django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
  60. django_cfg/modules/django_admin/utils/html/badges.py +47 -0
  61. django_cfg/modules/django_admin/utils/html/base.py +167 -0
  62. django_cfg/modules/django_admin/utils/html/code.py +87 -0
  63. django_cfg/modules/django_admin/utils/html/composition.py +198 -0
  64. django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
  65. django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
  66. django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
  67. django_cfg/modules/django_admin/utils/html/progress.py +127 -0
  68. django_cfg/modules/django_admin/utils/html_builder.py +55 -408
  69. django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
  70. django_cfg/modules/django_unfold/navigation.py +28 -0
  71. django_cfg/pyproject.toml +3 -5
  72. django_cfg/registry/modules.py +6 -0
  73. {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/METADATA +10 -1
  74. {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/RECORD +79 -30
  75. django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
  76. /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
  77. /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
  78. {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/WHEEL +0 -0
  79. {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/entry_points.txt +0 -0
  80. {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,515 @@
1
+ """
2
+ Request Logger Interceptor for gRPC.
3
+
4
+ Automatically logs all gRPC requests to the database for monitoring.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import time
11
+ import uuid
12
+ from typing import Callable
13
+
14
+ import grpc
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class RequestLoggerInterceptor(grpc.ServerInterceptor):
20
+ """
21
+ gRPC interceptor for request logging to database.
22
+
23
+ Features:
24
+ - Logs all requests to GRPCRequestLog model
25
+ - Captures timing, status, and error information
26
+ - Links requests to authenticated users
27
+ - Captures client metadata
28
+ - Tracks request/response sizes
29
+
30
+ Example:
31
+ ```python
32
+ # In Django settings (auto-configured)
33
+ GRPC_FRAMEWORK = {
34
+ "SERVER_INTERCEPTORS": [
35
+ "django_cfg.apps.grpc.interceptors.RequestLoggerInterceptor",
36
+ ]
37
+ }
38
+ ```
39
+
40
+ Database Schema:
41
+ All requests are logged to GRPCRequestLog model.
42
+ Use admin interface or REST API to view logs.
43
+ """
44
+
45
+ def __init__(self, log_request_data: bool = False, log_response_data: bool = False):
46
+ """
47
+ Initialize request logger.
48
+
49
+ Args:
50
+ log_request_data: Whether to log request data (default: False)
51
+ log_response_data: Whether to log response data (default: False)
52
+ """
53
+ self.log_request_data = log_request_data
54
+ self.log_response_data = log_response_data
55
+
56
+ def intercept_service(
57
+ self,
58
+ continuation: Callable,
59
+ handler_call_details: grpc.HandlerCallDetails,
60
+ ) -> grpc.RpcMethodHandler:
61
+ """
62
+ Intercept gRPC service call for logging.
63
+
64
+ Args:
65
+ continuation: Function to invoke the next interceptor or handler
66
+ handler_call_details: Details about the RPC call
67
+
68
+ Returns:
69
+ RPC method handler with logging
70
+ """
71
+ # Generate unique request ID
72
+ request_id = str(uuid.uuid4())
73
+
74
+ # Extract method info
75
+ full_method = handler_call_details.method
76
+ service_name, method_name = self._parse_method(full_method)
77
+
78
+ # Extract client metadata
79
+ metadata_dict = dict(handler_call_details.invocation_metadata)
80
+ peer = metadata_dict.get("peer", "unknown")
81
+ user_agent = metadata_dict.get("user-agent", None)
82
+
83
+ # Get handler and wrap it
84
+ handler = continuation(handler_call_details)
85
+
86
+ if handler is None:
87
+ logger.warning(f"[gRPC Logger] No handler found for {full_method}")
88
+ return None
89
+
90
+ # Wrap handler methods to log to database
91
+ return self._wrap_handler(
92
+ handler,
93
+ request_id,
94
+ service_name,
95
+ method_name,
96
+ full_method,
97
+ peer,
98
+ user_agent,
99
+ )
100
+
101
+ def _wrap_handler(
102
+ self,
103
+ handler: grpc.RpcMethodHandler,
104
+ request_id: str,
105
+ service_name: str,
106
+ method_name: str,
107
+ full_method: str,
108
+ peer: str,
109
+ user_agent: str,
110
+ ) -> grpc.RpcMethodHandler:
111
+ """
112
+ Wrap handler to add database logging.
113
+
114
+ Args:
115
+ handler: Original RPC method handler
116
+ request_id: Unique request ID
117
+ service_name: Service name
118
+ method_name: Method name
119
+ full_method: Full method path
120
+ peer: Client peer
121
+ user_agent: User agent
122
+
123
+ Returns:
124
+ Wrapped RPC method handler
125
+ """
126
+ def wrap_unary_unary(behavior):
127
+ def wrapper(request, context):
128
+ start_time = time.time()
129
+
130
+ # Create log entry
131
+ log_entry = self._create_log_entry(
132
+ request_id=request_id,
133
+ service_name=service_name,
134
+ method_name=method_name,
135
+ full_method=full_method,
136
+ peer=peer,
137
+ user_agent=user_agent,
138
+ context=context,
139
+ request=request if self.log_request_data else None,
140
+ )
141
+
142
+ try:
143
+ response = behavior(request, context)
144
+ duration_ms = int((time.time() - start_time) * 1000)
145
+
146
+ # Mark as successful
147
+ self._mark_success(
148
+ log_entry,
149
+ duration_ms=duration_ms,
150
+ response=response if self.log_response_data else None,
151
+ )
152
+
153
+ return response
154
+ except Exception as e:
155
+ duration_ms = int((time.time() - start_time) * 1000)
156
+
157
+ # Mark as error
158
+ self._mark_error(
159
+ log_entry,
160
+ error=e,
161
+ context=context,
162
+ duration_ms=duration_ms,
163
+ )
164
+
165
+ raise
166
+
167
+ return wrapper
168
+
169
+ def wrap_unary_stream(behavior):
170
+ def wrapper(request, context):
171
+ start_time = time.time()
172
+
173
+ # Create log entry
174
+ log_entry = self._create_log_entry(
175
+ request_id=request_id,
176
+ service_name=service_name,
177
+ method_name=method_name,
178
+ full_method=full_method,
179
+ peer=peer,
180
+ user_agent=user_agent,
181
+ context=context,
182
+ request=request if self.log_request_data else None,
183
+ )
184
+
185
+ try:
186
+ response_count = 0
187
+ for response in behavior(request, context):
188
+ response_count += 1
189
+ yield response
190
+
191
+ duration_ms = int((time.time() - start_time) * 1000)
192
+
193
+ # Mark as successful
194
+ self._mark_success(
195
+ log_entry,
196
+ duration_ms=duration_ms,
197
+ response_data={"message_count": response_count} if not self.log_response_data else None,
198
+ )
199
+
200
+ except Exception as e:
201
+ duration_ms = int((time.time() - start_time) * 1000)
202
+
203
+ # Mark as error
204
+ self._mark_error(
205
+ log_entry,
206
+ error=e,
207
+ context=context,
208
+ duration_ms=duration_ms,
209
+ )
210
+
211
+ raise
212
+
213
+ return wrapper
214
+
215
+ def wrap_stream_unary(behavior):
216
+ def wrapper(request_iterator, context):
217
+ start_time = time.time()
218
+
219
+ # Create log entry
220
+ log_entry = self._create_log_entry(
221
+ request_id=request_id,
222
+ service_name=service_name,
223
+ method_name=method_name,
224
+ full_method=full_method,
225
+ peer=peer,
226
+ user_agent=user_agent,
227
+ context=context,
228
+ )
229
+
230
+ try:
231
+ # Count requests
232
+ requests = []
233
+ request_count = 0
234
+ for req in request_iterator:
235
+ request_count += 1
236
+ requests.append(req)
237
+
238
+ # Process
239
+ response = behavior(iter(requests), context)
240
+ duration_ms = int((time.time() - start_time) * 1000)
241
+
242
+ # Mark as successful
243
+ self._mark_success(
244
+ log_entry,
245
+ duration_ms=duration_ms,
246
+ request_data={"message_count": request_count} if not self.log_request_data else None,
247
+ response=response if self.log_response_data else None,
248
+ )
249
+
250
+ return response
251
+ except Exception as e:
252
+ duration_ms = int((time.time() - start_time) * 1000)
253
+
254
+ # Mark as error
255
+ self._mark_error(
256
+ log_entry,
257
+ error=e,
258
+ context=context,
259
+ duration_ms=duration_ms,
260
+ )
261
+
262
+ raise
263
+
264
+ return wrapper
265
+
266
+ def wrap_stream_stream(behavior):
267
+ def wrapper(request_iterator, context):
268
+ start_time = time.time()
269
+
270
+ # Create log entry
271
+ log_entry = self._create_log_entry(
272
+ request_id=request_id,
273
+ service_name=service_name,
274
+ method_name=method_name,
275
+ full_method=full_method,
276
+ peer=peer,
277
+ user_agent=user_agent,
278
+ context=context,
279
+ )
280
+
281
+ try:
282
+ # Count requests
283
+ requests = []
284
+ request_count = 0
285
+ for req in request_iterator:
286
+ request_count += 1
287
+ requests.append(req)
288
+
289
+ # Process and count responses
290
+ response_count = 0
291
+ for response in behavior(iter(requests), context):
292
+ response_count += 1
293
+ yield response
294
+
295
+ duration_ms = int((time.time() - start_time) * 1000)
296
+
297
+ # Mark as successful
298
+ self._mark_success(
299
+ log_entry,
300
+ duration_ms=duration_ms,
301
+ response_data={"request_count": request_count, "response_count": response_count} if not self.log_response_data else None,
302
+ )
303
+
304
+ except Exception as e:
305
+ duration_ms = int((time.time() - start_time) * 1000)
306
+
307
+ # Mark as error
308
+ self._mark_error(
309
+ log_entry,
310
+ error=e,
311
+ context=context,
312
+ duration_ms=duration_ms,
313
+ )
314
+
315
+ raise
316
+
317
+ return wrapper
318
+
319
+ # Return wrapped handler based on type
320
+ if handler.unary_unary:
321
+ return grpc.unary_unary_rpc_method_handler(
322
+ wrap_unary_unary(handler.unary_unary),
323
+ request_deserializer=handler.request_deserializer,
324
+ response_serializer=handler.response_serializer,
325
+ )
326
+ elif handler.unary_stream:
327
+ return grpc.unary_stream_rpc_method_handler(
328
+ wrap_unary_stream(handler.unary_stream),
329
+ request_deserializer=handler.request_deserializer,
330
+ response_serializer=handler.response_serializer,
331
+ )
332
+ elif handler.stream_unary:
333
+ return grpc.stream_unary_rpc_method_handler(
334
+ wrap_stream_unary(handler.stream_unary),
335
+ request_deserializer=handler.request_deserializer,
336
+ response_serializer=handler.response_serializer,
337
+ )
338
+ elif handler.stream_stream:
339
+ return grpc.stream_stream_rpc_method_handler(
340
+ wrap_stream_stream(handler.stream_stream),
341
+ request_deserializer=handler.request_deserializer,
342
+ response_serializer=handler.response_serializer,
343
+ )
344
+ else:
345
+ return handler
346
+
347
+ def _create_log_entry(
348
+ self,
349
+ request_id: str,
350
+ service_name: str,
351
+ method_name: str,
352
+ full_method: str,
353
+ peer: str,
354
+ user_agent: str,
355
+ context: grpc.ServicerContext,
356
+ request=None,
357
+ ):
358
+ """Create initial log entry in database."""
359
+ try:
360
+ from ..models import GRPCRequestLog
361
+
362
+ # Get user from context (set by JWTAuthInterceptor)
363
+ user = getattr(context, "user", None)
364
+ is_authenticated = user is not None
365
+
366
+ # Extract client IP from peer
367
+ client_ip = self._extract_ip_from_peer(peer)
368
+
369
+ # Create log entry
370
+ log_entry = GRPCRequestLog.objects.create(
371
+ request_id=request_id,
372
+ service_name=service_name,
373
+ method_name=method_name,
374
+ full_method=full_method,
375
+ user=user if is_authenticated else None,
376
+ is_authenticated=is_authenticated,
377
+ client_ip=client_ip,
378
+ user_agent=user_agent,
379
+ peer=peer,
380
+ request_data=self._serialize_message(request) if request else None,
381
+ status="pending",
382
+ )
383
+
384
+ return log_entry
385
+
386
+ except Exception as e:
387
+ logger.error(f"Failed to create log entry: {e}", exc_info=True)
388
+ return None
389
+
390
+ def _mark_success(
391
+ self,
392
+ log_entry,
393
+ duration_ms: int,
394
+ response=None,
395
+ request_data: dict = None,
396
+ response_data: dict = None,
397
+ ):
398
+ """Mark log entry as successful."""
399
+ if log_entry is None:
400
+ return
401
+
402
+ try:
403
+ from ..models import GRPCRequestLog
404
+
405
+ # Prepare response data
406
+ if response:
407
+ response_data = self._serialize_message(response)
408
+
409
+ GRPCRequestLog.objects.mark_success(
410
+ log_entry,
411
+ duration_ms=duration_ms,
412
+ response_data=response_data,
413
+ )
414
+
415
+ except Exception as e:
416
+ logger.error(f"Failed to mark success: {e}", exc_info=True)
417
+
418
+ def _mark_error(
419
+ self,
420
+ log_entry,
421
+ error: Exception,
422
+ context: grpc.ServicerContext,
423
+ duration_ms: int,
424
+ ):
425
+ """Mark log entry as error."""
426
+ if log_entry is None:
427
+ return
428
+
429
+ try:
430
+ from ..models import GRPCRequestLog
431
+
432
+ # Get gRPC status code
433
+ grpc_code = self._get_grpc_code(error, context)
434
+
435
+ GRPCRequestLog.objects.mark_error(
436
+ log_entry,
437
+ grpc_status_code=grpc_code,
438
+ error_message=str(error),
439
+ error_details={"type": type(error).__name__},
440
+ duration_ms=duration_ms,
441
+ )
442
+
443
+ except Exception as e:
444
+ logger.error(f"Failed to mark error: {e}", exc_info=True)
445
+
446
+ def _parse_method(self, full_method: str) -> tuple[str, str]:
447
+ """
448
+ Parse full method path into service and method names.
449
+
450
+ Args:
451
+ full_method: Full method path (e.g., /myapp.UserService/GetUser)
452
+
453
+ Returns:
454
+ (service_name, method_name) tuple
455
+ """
456
+ try:
457
+ parts = full_method.strip("/").split("/")
458
+ if len(parts) >= 2:
459
+ return parts[0], parts[1]
460
+ else:
461
+ return full_method, "unknown"
462
+ except Exception:
463
+ return full_method, "unknown"
464
+
465
+ def _extract_ip_from_peer(self, peer: str) -> str | None:
466
+ """
467
+ Extract IP address from peer string.
468
+
469
+ Args:
470
+ peer: Peer string (e.g., ipv4:127.0.0.1:12345)
471
+
472
+ Returns:
473
+ IP address or None
474
+ """
475
+ try:
476
+ if ":" in peer:
477
+ parts = peer.split(":")
478
+ # Handle ipv4:x.x.x.x:port format
479
+ if len(parts) >= 3 and parts[0] in ["ipv4", "ipv6"]:
480
+ return parts[1]
481
+ # Handle x.x.x.x:port format
482
+ elif len(parts) == 2:
483
+ return parts[0]
484
+ except Exception:
485
+ pass
486
+ return None
487
+
488
+ def _get_grpc_code(self, error: Exception, context: grpc.ServicerContext) -> str:
489
+ """Get gRPC status code from error."""
490
+ try:
491
+ # Check if error is a gRPC error
492
+ if hasattr(error, "code"):
493
+ return error.code().name
494
+
495
+ # Try to get from context
496
+ if hasattr(context, "_state") and hasattr(context._state, "code"):
497
+ return context._state.code.name
498
+
499
+ # Default to UNKNOWN
500
+ return "UNKNOWN"
501
+ except Exception:
502
+ return "UNKNOWN"
503
+
504
+ def _serialize_message(self, message) -> dict | None:
505
+ """Serialize protobuf message to dict."""
506
+ try:
507
+ # Try to use MessageToDict from google.protobuf
508
+ from google.protobuf.json_format import MessageToDict
509
+ return MessageToDict(message)
510
+ except Exception as e:
511
+ logger.debug(f"Failed to serialize message: {e}")
512
+ return None
513
+
514
+
515
+ __all__ = ["RequestLoggerInterceptor"]
@@ -0,0 +1 @@
1
+ """Management commands for gRPC app."""
File without changes