django-cfg 1.5.20__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 (88) 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 +47 -0
  5. django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
  6. django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -37
  7. django_cfg/apps/integrations/centrifugo/views/wrapper.py +25 -23
  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 +21 -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} +215 -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/groups/manager.py +25 -18
  66. django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
  67. django_cfg/modules/django_logging/django_logger.py +58 -19
  68. django_cfg/pyproject.toml +3 -3
  69. django_cfg/static/frontend/admin.zip +0 -0
  70. django_cfg/templates/admin/index.html +0 -39
  71. django_cfg/utils/pool_monitor.py +320 -0
  72. django_cfg/utils/smart_defaults.py +233 -7
  73. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
  74. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/RECORD +87 -59
  75. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
  76. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
  77. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
  78. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
  79. /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
  80. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
  81. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
  82. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
  83. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
  84. /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
  85. /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
  86. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
  87. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
  88. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,170 @@
1
+ """
2
+ Global Service Registry for Streaming Services
3
+
4
+ Provides a global registry to store and retrieve BidirectionalStreamingService instances.
5
+ This enables same-process command sending from anywhere in the application.
6
+
7
+ Usage:
8
+ # In your gRPC service initialization (handlers/__init__.py)
9
+ from django_cfg.apps.integrations.grpc.commands.registry import register_streaming_service
10
+
11
+ def grpc_handlers(server):
12
+ servicer = YourStreamingService()
13
+ register_streaming_service("your_service", servicer._streaming_service)
14
+ # ... rest of setup
15
+
16
+ # In your command client
17
+ from django_cfg.apps.integrations.grpc.commands.registry import get_streaming_service
18
+
19
+ service = get_streaming_service("your_service")
20
+ client = CommandClient(client_id="123", streaming_service=service)
21
+
22
+ Documentation: See @commands/README.md § Global Service Registry
23
+ """
24
+
25
+ import logging
26
+ from threading import RLock
27
+ from typing import Any, Dict, List, Optional
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Global registry of streaming services
32
+ # Key: service name (e.g., "bots", "signals")
33
+ # Value: BidirectionalStreamingService instance
34
+ _streaming_services: Dict[str, Any] = {}
35
+
36
+ # Thread-safe lock for registry access
37
+ _registry_lock = RLock()
38
+
39
+
40
+ def register_streaming_service(name: str, service: Any) -> None:
41
+ """
42
+ Register a streaming service in the global registry.
43
+
44
+ This should be called when the gRPC service is initialized,
45
+ typically in the grpc_handlers() function.
46
+
47
+ Args:
48
+ name: Service name (e.g., "bots", "signals", "notifications")
49
+ service: BidirectionalStreamingService instance
50
+
51
+ Example:
52
+ from your_app.grpc.server import YourStreamingService
53
+
54
+ def grpc_handlers(server):
55
+ servicer = YourStreamingService()
56
+ register_streaming_service("your_service", servicer._streaming_service)
57
+ # ... rest of setup
58
+ """
59
+ with _registry_lock:
60
+ if name in _streaming_services:
61
+ logger.warning(
62
+ f"Streaming service '{name}' already registered, overwriting"
63
+ )
64
+
65
+ _streaming_services[name] = service
66
+ logger.info(f"Registered streaming service: {name}")
67
+
68
+
69
+ def get_streaming_service(name: str) -> Optional[Any]:
70
+ """
71
+ Get a streaming service from the global registry.
72
+
73
+ Args:
74
+ name: Service name
75
+
76
+ Returns:
77
+ BidirectionalStreamingService instance or None if not found
78
+
79
+ Example:
80
+ service = get_streaming_service("bots")
81
+ if service:
82
+ client = CommandClient(client_id="123", streaming_service=service)
83
+ else:
84
+ # Service not registered, will use cross-process mode
85
+ client = CommandClient(client_id="123", grpc_port=50051)
86
+ """
87
+ with _registry_lock:
88
+ service = _streaming_services.get(name)
89
+
90
+ if service is None:
91
+ logger.debug(
92
+ f"Streaming service '{name}' not found in registry. "
93
+ f"Available: {list(_streaming_services.keys())}"
94
+ )
95
+
96
+ return service
97
+
98
+
99
+ def unregister_streaming_service(name: str) -> bool:
100
+ """
101
+ Unregister a streaming service from the global registry.
102
+
103
+ Args:
104
+ name: Service name
105
+
106
+ Returns:
107
+ True if service was unregistered, False if not found
108
+ """
109
+ with _registry_lock:
110
+ if name in _streaming_services:
111
+ del _streaming_services[name]
112
+ logger.info(f"Unregistered streaming service: {name}")
113
+ return True
114
+ return False
115
+
116
+
117
+ def list_streaming_services() -> List[str]:
118
+ """
119
+ List all registered streaming service names.
120
+
121
+ Returns:
122
+ List of service names
123
+
124
+ Example:
125
+ >>> list_streaming_services()
126
+ ['bots', 'signals', 'notifications']
127
+ """
128
+ with _registry_lock:
129
+ return list(_streaming_services.keys())
130
+
131
+
132
+ def is_registered(name: str) -> bool:
133
+ """
134
+ Check if a streaming service is registered.
135
+
136
+ Args:
137
+ name: Service name
138
+
139
+ Returns:
140
+ True if registered, False otherwise
141
+ """
142
+ with _registry_lock:
143
+ return name in _streaming_services
144
+
145
+
146
+ def clear_registry() -> None:
147
+ """
148
+ Clear all registered services.
149
+
150
+ Warning: This should only be used in tests or during shutdown.
151
+ """
152
+ with _registry_lock:
153
+ count = len(_streaming_services)
154
+ _streaming_services.clear()
155
+ logger.info(f"Cleared {count} streaming services from registry")
156
+
157
+
158
+ # Convenience alias for backward compatibility
159
+ set_streaming_service = register_streaming_service
160
+
161
+
162
+ __all__ = [
163
+ 'register_streaming_service',
164
+ 'get_streaming_service',
165
+ 'unregister_streaming_service',
166
+ 'list_streaming_services',
167
+ 'is_registered',
168
+ 'clear_registry',
169
+ 'set_streaming_service', # alias
170
+ ]
@@ -0,0 +1,39 @@
1
+ """
2
+ gRPC service discovery and registry.
3
+
4
+ This package provides tools for discovering and registering gRPC services
5
+ in a distributed environment.
6
+
7
+ **Components**:
8
+ - discovery: Service discovery mechanisms
9
+ - registry: Service registration and management
10
+
11
+ **Usage Example**:
12
+ ```python
13
+ from django_cfg.apps.integrations.grpc.services.discovery import (
14
+ ServiceDiscovery,
15
+ ServiceRegistry,
16
+ )
17
+
18
+ # Register service
19
+ registry = ServiceRegistry()
20
+ registry.register(service_name="my-service", host="localhost", port=50051)
21
+
22
+ # Discover services
23
+ discovery = ServiceDiscovery()
24
+ services = discovery.discover_all()
25
+ ```
26
+
27
+ Created: 2025-11-07
28
+ Status: %%PRODUCTION%%
29
+ """
30
+
31
+ # Export discovery components
32
+ from .discovery import ServiceDiscovery, discover_and_register_services
33
+ from .registry import ServiceRegistryManager
34
+
35
+ __all__ = [
36
+ 'ServiceDiscovery',
37
+ 'ServiceRegistryManager',
38
+ 'discover_and_register_services',
39
+ ]
@@ -11,7 +11,7 @@ from django.apps import apps
11
11
 
12
12
  from django_cfg.modules.django_logging import get_logger
13
13
 
14
- from .config_helper import get_enabled_apps, get_grpc_config
14
+ from ..management.config_helper import get_enabled_apps, get_grpc_config
15
15
 
16
16
  logger = get_logger("grpc.discovery")
17
17
 
@@ -443,70 +443,77 @@ class ServiceDiscovery:
443
443
  logger.error(f"Error extracting metadata from {service_class}: {e}", exc_info=True)
444
444
  return None
445
445
 
446
- def get_handlers_hook(self) -> Optional[Any]:
446
+ def get_handlers_hooks(self) -> List[Any]:
447
447
  """
448
- Get the handlers hook function from config.
448
+ Get the handlers hook function(s) from config.
449
449
 
450
450
  Returns:
451
- Handlers hook function or None
451
+ List of handlers hook functions (empty list if none)
452
452
 
453
453
  Example:
454
454
  >>> discovery = ServiceDiscovery()
455
- >>> handlers_hook = discovery.get_handlers_hook()
456
- >>> if handlers_hook:
457
- ... services = handlers_hook(server)
455
+ >>> handlers_hooks = discovery.get_handlers_hooks()
456
+ >>> for hook in handlers_hooks:
457
+ ... hook(server)
458
458
  """
459
- logger.warning(f"🔍 get_handlers_hook called")
459
+ logger.warning(f"🔍 get_handlers_hooks called")
460
460
  if not self.config:
461
461
  logger.warning("❌ No gRPC config available")
462
- return None
463
-
464
- handlers_hook_path = self.config.handlers_hook
465
- logger.warning(f"🔍 handlers_hook_path = '{handlers_hook_path}'")
466
-
467
- if not handlers_hook_path:
468
- logger.debug("No handlers_hook configured")
469
- return None
462
+ return []
463
+
464
+ handlers_hook_paths = self.config.handlers_hook
465
+ logger.warning(f"🔍 handlers_hook_paths = '{handlers_hook_paths}'")
466
+
467
+ # Convert single string to list
468
+ if isinstance(handlers_hook_paths, str):
469
+ if not handlers_hook_paths:
470
+ logger.debug("No handlers_hook configured")
471
+ return []
472
+ handlers_hook_paths = [handlers_hook_paths]
473
+
474
+ hooks = []
475
+ for handlers_hook_path in handlers_hook_paths:
476
+ # Resolve {ROOT_URLCONF} placeholder
477
+ if "{ROOT_URLCONF}" in handlers_hook_path:
478
+ try:
479
+ from django.conf import settings
480
+ root_urlconf = settings.ROOT_URLCONF
481
+ handlers_hook_path = handlers_hook_path.replace("{ROOT_URLCONF}", root_urlconf)
482
+ logger.debug(f"Resolved handlers_hook: {handlers_hook_path}")
483
+ except Exception as e:
484
+ logger.warning(f"Could not resolve {{ROOT_URLCONF}}: {e}")
485
+ continue
470
486
 
471
- # Resolve {ROOT_URLCONF} placeholder
472
- if "{ROOT_URLCONF}" in handlers_hook_path:
473
487
  try:
474
- from django.conf import settings
475
- root_urlconf = settings.ROOT_URLCONF
476
- handlers_hook_path = handlers_hook_path.replace("{ROOT_URLCONF}", root_urlconf)
477
- logger.debug(f"Resolved handlers_hook: {handlers_hook_path}")
478
- except Exception as e:
479
- logger.warning(f"Could not resolve {{ROOT_URLCONF}}: {e}")
480
- return None
488
+ # Import the module containing the hook
489
+ module_path, func_name = handlers_hook_path.rsplit(".", 1)
490
+ module = importlib.import_module(module_path)
481
491
 
482
- try:
483
- # Import the module containing the hook
484
- module_path, func_name = handlers_hook_path.rsplit(".", 1)
485
- module = importlib.import_module(module_path)
492
+ # Get the hook function
493
+ handlers_hook = getattr(module, func_name)
486
494
 
487
- # Get the hook function
488
- handlers_hook = getattr(module, func_name)
495
+ if not callable(handlers_hook):
496
+ logger.warning(f"handlers_hook {handlers_hook_path} is not callable")
497
+ continue
489
498
 
490
- if not callable(handlers_hook):
491
- logger.warning(f"ROOT_HANDLERS_HOOK {handlers_hook_path} is not callable")
492
- return None
499
+ logger.info(f"Loaded handlers hook: {handlers_hook_path}")
500
+ hooks.append(handlers_hook)
493
501
 
494
- logger.info(f"Loaded handlers hook: {handlers_hook_path}")
495
- return handlers_hook
502
+ except ImportError as e:
503
+ logger.warning(f"Failed to import handlers hook module {handlers_hook_path}: {e}")
504
+ continue
505
+ except AttributeError as e:
506
+ logger.warning(f"Handlers hook function not found in {handlers_hook_path}: {e}")
507
+ logger.debug(f"This is optional - the hook function does not exist")
508
+ continue
509
+ except Exception as e:
510
+ logger.error(
511
+ f"Error loading handlers hook {handlers_hook_path}: {e}",
512
+ exc_info=True,
513
+ )
514
+ continue
496
515
 
497
- except ImportError as e:
498
- logger.warning(f"Failed to import handlers hook module {handlers_hook_path}: {e}")
499
- return None
500
- except AttributeError as e:
501
- logger.warning(f"Handlers hook function not found in {handlers_hook_path}: {e}")
502
- logger.debug(f"This is optional - the hook function '{func_name}' does not exist in module '{module_path}'")
503
- return None
504
- except Exception as e:
505
- logger.error(
506
- f"Error loading handlers hook {handlers_hook_path}: {e}",
507
- exc_info=True,
508
- )
509
- return None
516
+ return hooks
510
517
 
511
518
 
512
519
  def discover_and_register_services(server: Any) -> int:
@@ -540,15 +547,15 @@ def discover_and_register_services(server: Any) -> int:
540
547
  discovery = ServiceDiscovery()
541
548
  count = 0
542
549
 
543
- # Try handlers hook first
544
- handlers_hook = discovery.get_handlers_hook()
545
- if handlers_hook:
550
+ # Try handlers hooks first (can be multiple)
551
+ handlers_hooks = discovery.get_handlers_hooks()
552
+ for hook in handlers_hooks:
546
553
  try:
547
- handlers_hook(server)
548
- logger.info("Successfully called handlers hook")
554
+ hook(server)
555
+ logger.info(f"Successfully called handlers hook: {hook.__name__}")
549
556
  count += 1 # We don't know exact count, but at least 1
550
557
  except Exception as e:
551
- logger.error(f"Error calling handlers hook: {e}", exc_info=True)
558
+ logger.error(f"Error calling handlers hook {hook.__name__}: {e}", exc_info=True)
552
559
 
553
560
  # Discover and register services
554
561
  services = discovery.discover_services()
@@ -10,7 +10,7 @@ from typing import Dict, List, Optional
10
10
  from django.db import models
11
11
  from django.db.models import Avg, Count
12
12
 
13
- from ..models import GRPCRequestLog, GRPCServerStatus
13
+ from ...models import GRPCRequestLog, GRPCServerStatus
14
14
  from django_cfg.modules.django_logging import get_logger
15
15
 
16
16
  logger = get_logger("grpc.service_registry")
@@ -31,7 +31,7 @@ class ServiceRegistryManager:
31
31
 
32
32
  def get_current_server(self) -> Optional[GRPCServerStatus]:
33
33
  """
34
- Get the currently running gRPC server instance.
34
+ Get the currently running gRPC server instance (SYNC).
35
35
 
36
36
  Returns:
37
37
  GRPCServerStatus instance if server is running, None otherwise
@@ -45,6 +45,22 @@ class ServiceRegistryManager:
45
45
  logger.error(f"Error getting current server: {e}", exc_info=True)
46
46
  return None
47
47
 
48
+ async def aget_current_server(self) -> Optional[GRPCServerStatus]:
49
+ """
50
+ Get the currently running gRPC server instance (ASYNC - Django 5.2).
51
+
52
+ Returns:
53
+ GRPCServerStatus instance if server is running, None otherwise
54
+ """
55
+ try:
56
+ current_server = await GRPCServerStatus.objects.aget_current_server()
57
+ if current_server and current_server.is_running:
58
+ return current_server
59
+ return None
60
+ except Exception as e:
61
+ logger.error(f"Error getting current server: {e}", exc_info=True)
62
+ return None
63
+
48
64
  def get_all_services(self) -> List[Dict]:
49
65
  """
50
66
  Get all registered services.
@@ -100,7 +116,7 @@ class ServiceRegistryManager:
100
116
  self, service_name: str, hours: int = 24
101
117
  ) -> Dict:
102
118
  """
103
- Get statistics for a specific service.
119
+ Get statistics for a specific service (SYNC).
104
120
 
105
121
  Args:
106
122
  service_name: Service name
@@ -142,9 +158,39 @@ class ServiceRegistryManager:
142
158
  "avg_duration_ms": round(stats["avg_duration"] or 0, 2),
143
159
  }
144
160
 
161
+ async def aget_service_statistics(
162
+ self, service_name: str, hours: int = 24
163
+ ) -> Dict:
164
+ """
165
+ Get statistics for a specific service (ASYNC - Django 5.2).
166
+
167
+ Args:
168
+ service_name: Service name
169
+ hours: Statistics period in hours (default: 24)
170
+
171
+ Returns:
172
+ Dictionary with service statistics
173
+
174
+ Example:
175
+ >>> manager = ServiceRegistryManager()
176
+ >>> stats = await manager.aget_service_statistics("apps.CryptoService", hours=24)
177
+ >>> stats['total']
178
+ 150
179
+ >>> stats['successful']
180
+ 145
181
+ >>> stats['success_rate']
182
+ 96.67
183
+ """
184
+ # Django 5.2: Native async aggregate is not available yet (Django 5.2 limitation)
185
+ # Use asgiref.sync.sync_to_async as recommended by Django docs
186
+ from asgiref.sync import sync_to_async
187
+
188
+ # Wrap the sync version in sync_to_async
189
+ return await sync_to_async(self.get_service_statistics)(service_name, hours)
190
+
145
191
  def get_all_services_with_stats(self, hours: int = 24) -> List[Dict]:
146
192
  """
147
- Get all services with their statistics.
193
+ Get all services with their statistics (SYNC).
148
194
 
149
195
  Args:
150
196
  hours: Statistics period in hours (default: 24)
@@ -206,11 +252,84 @@ class ServiceRegistryManager:
206
252
 
207
253
  return services_with_stats
208
254
 
255
+ async def aget_all_services_with_stats(self, hours: int = 24) -> List[Dict]:
256
+ """
257
+ Get all services with their statistics (ASYNC - Django 5.2).
258
+
259
+ Args:
260
+ hours: Statistics period in hours (default: 24)
261
+
262
+ Returns:
263
+ List of services with statistics
264
+
265
+ Example:
266
+ >>> manager = ServiceRegistryManager()
267
+ >>> services = await manager.aget_all_services_with_stats(hours=24)
268
+ >>> services[0]['name']
269
+ 'apps.CryptoService'
270
+ >>> services[0]['total_requests']
271
+ 150
272
+ """
273
+ # Get all services (sync operation - from cache or filesystem)
274
+ services = self.get_all_services()
275
+ services_with_stats = []
276
+
277
+ # Django 5.2: Use sync_to_async for aggregate queries that aren't natively async yet
278
+ from asgiref.sync import sync_to_async
279
+
280
+ # Create async version of the aggregate operation
281
+ @sync_to_async
282
+ def get_service_stats_sync(service_name: str):
283
+ return (
284
+ GRPCRequestLog.objects.filter(service_name=service_name)
285
+ .recent(hours)
286
+ .aggregate(
287
+ total=Count("id"),
288
+ successful=Count("id", filter=models.Q(status="success")),
289
+ avg_duration=Avg("duration_ms"),
290
+ last_activity=models.Max("created_at"),
291
+ )
292
+ )
293
+
294
+ for service in services:
295
+ service_name = service.get("name")
296
+
297
+ # Get stats from GRPCRequestLog (async)
298
+ stats = await get_service_stats_sync(service_name)
299
+
300
+ # Calculate success rate
301
+ total = stats["total"] or 0
302
+ successful = stats["successful"] or 0
303
+ success_rate = (successful / total * 100) if total > 0 else 0.0
304
+
305
+ # Extract package name
306
+ package = service_name.split(".")[0] if "." in service_name else ""
307
+
308
+ # Build dict directly
309
+ service_summary = {
310
+ "name": service_name,
311
+ "full_name": service.get("full_name", f"/{service_name}"),
312
+ "package": package,
313
+ "methods_count": len(service.get("methods", [])),
314
+ "total_requests": total,
315
+ "success_rate": round(success_rate, 2),
316
+ "avg_duration_ms": round(stats["avg_duration"] or 0, 2),
317
+ "last_activity_at": (
318
+ stats["last_activity"].isoformat()
319
+ if stats["last_activity"]
320
+ else None
321
+ ),
322
+ }
323
+
324
+ services_with_stats.append(service_summary)
325
+
326
+ return services_with_stats
327
+
209
328
  def get_service_methods_with_stats(
210
329
  self, service_name: str
211
330
  ) -> List[Dict]:
212
331
  """
213
- Get all methods for a service with statistics.
332
+ Get all methods for a service with statistics (SYNC).
214
333
 
215
334
  Args:
216
335
  service_name: Service name
@@ -286,6 +405,97 @@ class ServiceRegistryManager:
286
405
 
287
406
  return methods_list
288
407
 
408
+ async def aget_service_methods_with_stats(
409
+ self, service_name: str
410
+ ) -> List[Dict]:
411
+ """
412
+ Get all methods for a service with statistics (ASYNC - Django 5.2).
413
+
414
+ Args:
415
+ service_name: Service name
416
+
417
+ Returns:
418
+ List of methods with statistics
419
+
420
+ Example:
421
+ >>> manager = ServiceRegistryManager()
422
+ >>> methods = await manager.aget_service_methods_with_stats("apps.CryptoService")
423
+ >>> methods[0]['name']
424
+ 'GetCoin'
425
+ >>> methods[0]['stats']['total_requests']
426
+ 50
427
+ """
428
+ service = self.get_service_by_name(service_name)
429
+ if not service:
430
+ return []
431
+
432
+ # Django 5.2: Use sync_to_async for complex queries with aggregates and values_list
433
+ from asgiref.sync import sync_to_async
434
+
435
+ @sync_to_async
436
+ def get_method_durations(svc_name: str, method_name: str):
437
+ return list(
438
+ GRPCRequestLog.objects.filter(
439
+ service_name=svc_name,
440
+ method_name=method_name,
441
+ duration_ms__isnull=False,
442
+ ).values_list("duration_ms", flat=True)
443
+ )
444
+
445
+ @sync_to_async
446
+ def get_method_stats(svc_name: str, method_name: str):
447
+ return GRPCRequestLog.objects.filter(
448
+ service_name=svc_name,
449
+ method_name=method_name,
450
+ ).aggregate(
451
+ total=Count("id"),
452
+ successful=Count("id", filter=models.Q(status="success")),
453
+ errors=Count("id", filter=models.Q(status="error")),
454
+ avg_duration=Avg("duration_ms"),
455
+ )
456
+
457
+ methods_list = []
458
+ for method_name in service.get("methods", []):
459
+ # Get durations for percentile calculation (async)
460
+ durations = await get_method_durations(service_name, method_name)
461
+
462
+ # Get aggregate stats (async)
463
+ stats = await get_method_stats(service_name, method_name)
464
+
465
+ # Calculate percentiles
466
+ p50, p95, p99 = self._calculate_percentiles(durations)
467
+
468
+ # Calculate success rate
469
+ total = stats["total"] or 0
470
+ successful = stats["successful"] or 0
471
+ success_rate = (successful / total * 100) if total > 0 else 0.0
472
+
473
+ # Build method stats dict
474
+ method_stats = {
475
+ "total_requests": total,
476
+ "successful": successful,
477
+ "errors": stats["errors"] or 0,
478
+ "success_rate": round(success_rate, 2),
479
+ "avg_duration_ms": round(stats["avg_duration"] or 0, 2),
480
+ "p50_duration_ms": p50,
481
+ "p95_duration_ms": p95,
482
+ "p99_duration_ms": p99,
483
+ }
484
+
485
+ # Build method summary dict
486
+ method_summary = {
487
+ "name": method_name,
488
+ "full_name": f"/{service_name}/{method_name}",
489
+ "service_name": service_name,
490
+ "request_type": "",
491
+ "response_type": "",
492
+ "stats": method_stats,
493
+ }
494
+
495
+ methods_list.append(method_summary)
496
+
497
+ return methods_list
498
+
289
499
  def _calculate_percentiles(self, values):
290
500
  """
291
501
  Calculate p50, p95, p99 percentiles.
@@ -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']}")