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,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
 
@@ -53,6 +53,10 @@ class ServiceDiscovery:
53
53
  # Get config from django-cfg using Pydantic2 pattern
54
54
  self.config = get_grpc_config()
55
55
 
56
+ logger.warning(f"🔍 ServiceDiscovery.__init__: config = {self.config}")
57
+ if self.config:
58
+ logger.warning(f"🔍 handlers_hook = {self.config.handlers_hook}")
59
+
56
60
  if self.config:
57
61
  self.auto_register = self.config.auto_register_apps
58
62
  self.enabled_apps = self.config.enabled_apps if self.config.auto_register_apps else []
@@ -439,68 +443,77 @@ class ServiceDiscovery:
439
443
  logger.error(f"Error extracting metadata from {service_class}: {e}", exc_info=True)
440
444
  return None
441
445
 
442
- def get_handlers_hook(self) -> Optional[Any]:
446
+ def get_handlers_hooks(self) -> List[Any]:
443
447
  """
444
- Get the handlers hook function from config.
448
+ Get the handlers hook function(s) from config.
445
449
 
446
450
  Returns:
447
- Handlers hook function or None
451
+ List of handlers hook functions (empty list if none)
448
452
 
449
453
  Example:
450
454
  >>> discovery = ServiceDiscovery()
451
- >>> handlers_hook = discovery.get_handlers_hook()
452
- >>> if handlers_hook:
453
- ... services = handlers_hook(server)
455
+ >>> handlers_hooks = discovery.get_handlers_hooks()
456
+ >>> for hook in handlers_hooks:
457
+ ... hook(server)
454
458
  """
459
+ logger.warning(f"🔍 get_handlers_hooks called")
455
460
  if not self.config:
456
- logger.debug("No gRPC config available")
457
- return None
458
-
459
- handlers_hook_path = self.config.handlers_hook
460
-
461
- if not handlers_hook_path:
462
- logger.debug("No handlers_hook configured")
463
- return None
461
+ logger.warning("No gRPC config available")
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
464
486
 
465
- # Resolve {ROOT_URLCONF} placeholder
466
- if "{ROOT_URLCONF}" in handlers_hook_path:
467
487
  try:
468
- from django.conf import settings
469
- root_urlconf = settings.ROOT_URLCONF
470
- handlers_hook_path = handlers_hook_path.replace("{ROOT_URLCONF}", root_urlconf)
471
- logger.debug(f"Resolved handlers_hook: {handlers_hook_path}")
472
- except Exception as e:
473
- logger.warning(f"Could not resolve {{ROOT_URLCONF}}: {e}")
474
- 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)
475
491
 
476
- try:
477
- # Import the module containing the hook
478
- module_path, func_name = handlers_hook_path.rsplit(".", 1)
479
- module = importlib.import_module(module_path)
492
+ # Get the hook function
493
+ handlers_hook = getattr(module, func_name)
480
494
 
481
- # Get the hook function
482
- 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
483
498
 
484
- if not callable(handlers_hook):
485
- logger.warning(f"ROOT_HANDLERS_HOOK {handlers_hook_path} is not callable")
486
- return None
499
+ logger.info(f"Loaded handlers hook: {handlers_hook_path}")
500
+ hooks.append(handlers_hook)
487
501
 
488
- logger.info(f"Loaded handlers hook: {handlers_hook_path}")
489
- 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
490
515
 
491
- except ImportError as e:
492
- logger.warning(f"Failed to import handlers hook module {handlers_hook_path}: {e}")
493
- return None
494
- except AttributeError as e:
495
- logger.warning(f"Handlers hook function not found in {handlers_hook_path}: {e}")
496
- logger.debug(f"This is optional - the hook function '{func_name}' does not exist in module '{module_path}'")
497
- return None
498
- except Exception as e:
499
- logger.error(
500
- f"Error loading handlers hook {handlers_hook_path}: {e}",
501
- exc_info=True,
502
- )
503
- return None
516
+ return hooks
504
517
 
505
518
 
506
519
  def discover_and_register_services(server: Any) -> int:
@@ -534,15 +547,15 @@ def discover_and_register_services(server: Any) -> int:
534
547
  discovery = ServiceDiscovery()
535
548
  count = 0
536
549
 
537
- # Try handlers hook first
538
- handlers_hook = discovery.get_handlers_hook()
539
- if handlers_hook:
550
+ # Try handlers hooks first (can be multiple)
551
+ handlers_hooks = discovery.get_handlers_hooks()
552
+ for hook in handlers_hooks:
540
553
  try:
541
- handlers_hook(server)
542
- logger.info("Successfully called handlers hook")
554
+ hook(server)
555
+ logger.info(f"Successfully called handlers hook: {hook.__name__}")
543
556
  count += 1 # We don't know exact count, but at least 1
544
557
  except Exception as e:
545
- 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)
546
559
 
547
560
  # Discover and register services
548
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.
@@ -1,15 +1,17 @@
1
1
  """
2
- gRPC interceptors for logging, metrics, and error handling.
2
+ gRPC interceptors for logging, metrics, error handling, and Centrifugo publishing.
3
3
 
4
4
  Provides production-ready interceptors for gRPC services.
5
5
  """
6
6
 
7
+ from .centrifugo import CentrifugoInterceptor
7
8
  from .errors import ErrorHandlingInterceptor
8
9
  from .logging import LoggingInterceptor
9
10
  from .metrics import MetricsInterceptor, get_metrics, reset_metrics
10
11
  from .request_logger import RequestLoggerInterceptor
11
12
 
12
13
  __all__ = [
14
+ "CentrifugoInterceptor",
13
15
  "LoggingInterceptor",
14
16
  "MetricsInterceptor",
15
17
  "ErrorHandlingInterceptor",