django-cfg 1.5.8__py3-none-any.whl → 1.5.14__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 (119) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/commands/serializers.py +152 -0
  3. django_cfg/apps/api/commands/views.py +32 -0
  4. django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
  5. django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
  6. django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
  7. django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
  8. django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
  9. django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
  10. django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
  11. django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
  12. django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
  13. django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
  14. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +5 -5
  15. django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
  16. django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
  17. django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
  18. django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
  19. django_cfg/apps/integrations/grpc/admin/config.py +113 -9
  20. django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
  21. django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
  22. django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
  23. django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
  24. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
  25. django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
  26. django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
  27. django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
  28. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +130 -0
  29. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +171 -96
  30. django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
  31. django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
  32. django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
  33. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
  34. django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
  35. django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
  36. django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
  37. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
  38. django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
  39. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
  40. django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
  41. django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
  42. django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
  43. django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
  44. django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
  45. django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
  46. django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
  47. django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
  48. django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
  49. django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
  50. django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
  51. django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
  52. django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
  53. django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
  54. django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
  55. django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
  56. django_cfg/apps/integrations/grpc/urls.py +8 -0
  57. django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
  58. django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
  59. django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
  60. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +177 -0
  61. django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
  62. django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
  63. django_cfg/apps/integrations/grpc/views/charts.py +21 -14
  64. django_cfg/apps/integrations/grpc/views/config.py +8 -6
  65. django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
  66. django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
  67. django_cfg/apps/integrations/grpc/views/services.py +30 -21
  68. django_cfg/apps/integrations/grpc/views/testing.py +45 -43
  69. django_cfg/apps/integrations/rq/views/jobs.py +19 -9
  70. django_cfg/apps/integrations/rq/views/schedule.py +7 -3
  71. django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
  72. django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
  73. django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
  74. django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
  75. django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
  76. django_cfg/config.py +33 -0
  77. django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
  78. django_cfg/management/commands/check_endpoints.py +2 -2
  79. django_cfg/management/commands/check_settings.py +3 -10
  80. django_cfg/management/commands/clear_constance.py +3 -10
  81. django_cfg/management/commands/create_token.py +4 -11
  82. django_cfg/management/commands/list_urls.py +4 -10
  83. django_cfg/management/commands/migrate_all.py +18 -12
  84. django_cfg/management/commands/migrator.py +4 -11
  85. django_cfg/management/commands/script.py +4 -10
  86. django_cfg/management/commands/show_config.py +8 -16
  87. django_cfg/management/commands/show_urls.py +5 -11
  88. django_cfg/management/commands/superuser.py +4 -11
  89. django_cfg/management/commands/tree.py +5 -10
  90. django_cfg/management/utils/README.md +402 -0
  91. django_cfg/management/utils/__init__.py +29 -0
  92. django_cfg/management/utils/mixins.py +176 -0
  93. django_cfg/middleware/pagination.py +53 -54
  94. django_cfg/models/api/grpc/__init__.py +15 -21
  95. django_cfg/models/api/grpc/config.py +155 -73
  96. django_cfg/models/ngrok/config.py +7 -6
  97. django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
  98. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
  99. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
  100. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
  101. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
  102. django_cfg/modules/django_client/core/parser/base.py +114 -30
  103. django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
  104. django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
  105. django_cfg/modules/django_email/management/commands/test_email.py +4 -10
  106. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
  107. django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
  108. django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
  109. django_cfg/modules/django_unfold/navigation.py +6 -18
  110. django_cfg/pyproject.toml +1 -1
  111. django_cfg/registry/modules.py +1 -4
  112. django_cfg/requirements.txt +52 -0
  113. django_cfg/static/frontend/admin.zip +0 -0
  114. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/METADATA +1 -1
  115. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/RECORD +118 -97
  116. django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
  117. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/WHEEL +0 -0
  118. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/entry_points.txt +0 -0
  119. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,320 @@
1
+ """
2
+ API Key Authentication Interceptor for gRPC.
3
+
4
+ Handles API key verification and Django user authentication for gRPC requests.
5
+ Simple, secure, and manageable through Django admin.
6
+ """
7
+
8
+ import asyncio
9
+ import contextvars
10
+ import logging
11
+ from typing import Callable, Optional
12
+
13
+ import grpc
14
+ import grpc.aio
15
+ from django.conf import settings
16
+ from django.contrib.auth import get_user_model
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ User = get_user_model()
21
+
22
+ # Context variables for passing user/api_key between async interceptors
23
+ _grpc_user_var: contextvars.ContextVar = contextvars.ContextVar('grpc_user', default=None)
24
+ _grpc_api_key_var: contextvars.ContextVar = contextvars.ContextVar('grpc_api_key', default=None)
25
+
26
+
27
+ class ApiKeyAuthInterceptor(grpc.aio.ServerInterceptor):
28
+ """
29
+ gRPC interceptor for API key authentication.
30
+
31
+ Features:
32
+ - Validates API keys from database (GrpcApiKey model)
33
+ - Accepts Django SECRET_KEY for development/internal use
34
+ - Loads Django user from API key
35
+ - Sets user on request context
36
+ - Supports public methods whitelist
37
+ - Tracks API key usage
38
+ - Handles authentication errors gracefully
39
+
40
+ Example:
41
+ ```python
42
+ # In Django settings (auto-configured by django-cfg)
43
+ GRPC_FRAMEWORK = {
44
+ "SERVER_INTERCEPTORS": [
45
+ "django_cfg.apps.integrations.grpc.auth.ApiKeyAuthInterceptor",
46
+ ]
47
+ }
48
+ ```
49
+
50
+ API Key Format:
51
+ x-api-key: <api_key_string>
52
+
53
+ Or for SECRET_KEY:
54
+ x-api-key: <Django SECRET_KEY>
55
+ """
56
+
57
+ def __init__(self):
58
+ """Initialize API key authentication interceptor."""
59
+ self.grpc_auth_config = getattr(settings, "GRPC_AUTH", {})
60
+ self.enabled = self.grpc_auth_config.get("enabled", True)
61
+ self.require_auth = self.grpc_auth_config.get("require_auth", False)
62
+
63
+ # API Key settings
64
+ self.api_key_header = self.grpc_auth_config.get("api_key_header", "x-api-key")
65
+ self.accept_django_secret_key = self.grpc_auth_config.get(
66
+ "accept_django_secret_key", True
67
+ )
68
+
69
+ # Public methods (don't require auth)
70
+ self.public_methods = self.grpc_auth_config.get("public_methods", [
71
+ "/grpc.health.v1.Health/Check",
72
+ "/grpc.health.v1.Health/Watch",
73
+ "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo",
74
+ ])
75
+
76
+ async def intercept_service(
77
+ self,
78
+ continuation: Callable,
79
+ handler_call_details: grpc.HandlerCallDetails
80
+ ) -> grpc.RpcMethodHandler:
81
+ """
82
+ Intercept gRPC service call for authentication (async).
83
+
84
+ Args:
85
+ continuation: Function to invoke the next interceptor or handler
86
+ handler_call_details: Details about the RPC call
87
+
88
+ Returns:
89
+ RPC method handler (possibly wrapped with auth)
90
+ """
91
+ # Skip if auth is disabled
92
+ if not self.enabled:
93
+ return await continuation(handler_call_details)
94
+
95
+ # Check if method is public
96
+ method_name = handler_call_details.method
97
+ if method_name in self.public_methods:
98
+ logger.debug(f"Public method accessed: {method_name}")
99
+ return await continuation(handler_call_details)
100
+
101
+ # Extract API key from metadata
102
+ api_key = self._extract_api_key(handler_call_details.invocation_metadata)
103
+
104
+ # If no API key provided
105
+ if not api_key:
106
+ if self.require_auth:
107
+ logger.warning(f"Missing API key for {method_name}")
108
+ return self._abort_unauthenticated("API key is required")
109
+ else:
110
+ # Allow anonymous access (no user/api_key in context)
111
+ logger.debug(f"No API key provided for {method_name}, allowing anonymous access")
112
+ return await continuation(handler_call_details)
113
+
114
+ # Verify API key and get user + api_key instance (async)
115
+ user, api_key_instance = await self._verify_api_key(api_key)
116
+
117
+ # If API key is valid, ALWAYS set user and api_key in context (even if require_auth=False)
118
+ if user:
119
+ logger.debug(f"Authenticated user {user.id} ({user.username}) for {method_name}")
120
+ return await self._continue_with_user(continuation, handler_call_details, user, api_key_instance)
121
+
122
+ # API key provided but invalid
123
+ if self.require_auth:
124
+ logger.warning(f"Invalid API key for {method_name}")
125
+ return self._abort_unauthenticated("Invalid or expired API key")
126
+ else:
127
+ # Allow anonymous access even with invalid key
128
+ logger.debug(f"Invalid API key for {method_name}, allowing anonymous access")
129
+ return await continuation(handler_call_details)
130
+
131
+ def _extract_api_key(self, metadata: tuple) -> Optional[str]:
132
+ """
133
+ Extract API key from gRPC metadata.
134
+
135
+ Args:
136
+ metadata: gRPC invocation metadata
137
+
138
+ Returns:
139
+ API key string or None
140
+ """
141
+ if not metadata:
142
+ return None
143
+
144
+ # Convert metadata to dict (case-insensitive lookup)
145
+ metadata_dict = dict(metadata)
146
+
147
+ # Get API key header (case-insensitive)
148
+ for key, value in metadata_dict.items():
149
+ if key.lower() == self.api_key_header.lower():
150
+ return value
151
+
152
+ return None
153
+
154
+ async def _verify_api_key(self, api_key: str) -> tuple[Optional[User], Optional["GrpcApiKey"]]:
155
+ """
156
+ Verify API key and return user and api_key instance (async).
157
+
158
+ Checks:
159
+ 1. Django SECRET_KEY (if enabled)
160
+ 2. GrpcApiKey model in database
161
+
162
+ Args:
163
+ api_key: API key string
164
+
165
+ Returns:
166
+ Tuple of (Django User instance or None, GrpcApiKey instance or None)
167
+ """
168
+ # Check if it's Django SECRET_KEY
169
+ if self.accept_django_secret_key and api_key == settings.SECRET_KEY:
170
+ logger.debug("API key matches Django SECRET_KEY")
171
+ # For SECRET_KEY, return first superuser or None (no api_key instance)
172
+ try:
173
+ # Wrap Django ORM in asyncio.to_thread()
174
+ superuser = await asyncio.to_thread(
175
+ lambda: User.objects.filter(is_superuser=True, is_active=True).first()
176
+ )
177
+ if superuser:
178
+ return superuser, None
179
+ else:
180
+ logger.warning("No active superuser found for SECRET_KEY authentication")
181
+ return None, None
182
+ except Exception as e:
183
+ logger.error(f"Error loading superuser for SECRET_KEY: {e}")
184
+ return None, None
185
+
186
+ # Check API key in database
187
+ try:
188
+ from django_cfg.apps.integrations.grpc.models import GrpcApiKey
189
+
190
+ # Wrap Django ORM in asyncio.to_thread()
191
+ api_key_obj = await asyncio.to_thread(
192
+ lambda: GrpcApiKey.objects.filter(key=api_key, is_active=True).first()
193
+ )
194
+
195
+ if api_key_obj and api_key_obj.is_valid:
196
+ # Update usage tracking (also wrapped in to_thread)
197
+ await asyncio.to_thread(api_key_obj.mark_used)
198
+ logger.debug(f"Valid API key for user {api_key_obj.user.id} ({api_key_obj.user.username})")
199
+ return api_key_obj.user, api_key_obj
200
+ else:
201
+ logger.debug("API key not found or invalid in database")
202
+ return None, None
203
+
204
+ except Exception as e:
205
+ logger.error(f"Error validating API key: {e}")
206
+ return None, None
207
+
208
+ async def _continue_with_user(
209
+ self,
210
+ continuation: Callable,
211
+ handler_call_details: grpc.HandlerCallDetails,
212
+ user: User,
213
+ api_key_instance: Optional["GrpcApiKey"] = None,
214
+ ) -> grpc.RpcMethodHandler:
215
+ """
216
+ Continue RPC with authenticated user and api_key in context (async).
217
+
218
+ Args:
219
+ continuation: Function to invoke next interceptor or handler
220
+ handler_call_details: Details about the RPC call
221
+ user: Authenticated Django user
222
+ api_key_instance: GrpcApiKey instance used for authentication (if applicable)
223
+
224
+ Returns:
225
+ RPC method handler with user and api_key context
226
+ """
227
+ # Get the handler (await because continuation is async)
228
+ handler = await continuation(handler_call_details)
229
+
230
+ if handler is None:
231
+ return None
232
+
233
+ # Wrap the handler to inject user and api_key into contextvars (not context directly)
234
+ # All wrappers must be async for grpc.aio
235
+ async def wrapped_unary_unary(request, context):
236
+ # Set context variables for async context
237
+ _grpc_user_var.set(user)
238
+ _grpc_api_key_var.set(api_key_instance)
239
+ logger.info(f"[Auth] Set contextvar api_key = {api_key_instance} (user={user})")
240
+ return await handler.unary_unary(request, context)
241
+
242
+ async def wrapped_unary_stream(request, context):
243
+ # Set context variables for async context
244
+ _grpc_user_var.set(user)
245
+ _grpc_api_key_var.set(api_key_instance)
246
+ logger.info(f"[Auth] Set contextvar api_key = {api_key_instance} (user={user})")
247
+ async for response in handler.unary_stream(request, context):
248
+ yield response
249
+
250
+ async def wrapped_stream_unary(request_iterator, context):
251
+ # Set context variables for async context
252
+ _grpc_user_var.set(user)
253
+ _grpc_api_key_var.set(api_key_instance)
254
+ return await handler.stream_unary(request_iterator, context)
255
+
256
+ async def wrapped_stream_stream(request_iterator, context):
257
+ # Set context variables for async context
258
+ _grpc_user_var.set(user)
259
+ _grpc_api_key_var.set(api_key_instance)
260
+ async for response in handler.stream_stream(request_iterator, context):
261
+ yield response
262
+
263
+ # Return wrapped handler based on type
264
+ return grpc.unary_unary_rpc_method_handler(
265
+ wrapped_unary_unary,
266
+ request_deserializer=handler.request_deserializer,
267
+ response_serializer=handler.response_serializer,
268
+ ) if handler.unary_unary else (
269
+ grpc.unary_stream_rpc_method_handler(
270
+ wrapped_unary_stream,
271
+ request_deserializer=handler.request_deserializer,
272
+ response_serializer=handler.response_serializer,
273
+ ) if handler.unary_stream else (
274
+ grpc.stream_unary_rpc_method_handler(
275
+ wrapped_stream_unary,
276
+ request_deserializer=handler.request_deserializer,
277
+ response_serializer=handler.response_serializer,
278
+ ) if handler.stream_unary else (
279
+ grpc.stream_stream_rpc_method_handler(
280
+ wrapped_stream_stream,
281
+ request_deserializer=handler.request_deserializer,
282
+ response_serializer=handler.response_serializer,
283
+ ) if handler.stream_stream else None
284
+ )
285
+ )
286
+ )
287
+
288
+ def _abort_unauthenticated(self, message: str) -> grpc.RpcMethodHandler:
289
+ """
290
+ Return handler that aborts with UNAUTHENTICATED status.
291
+
292
+ Args:
293
+ message: Error message
294
+
295
+ Returns:
296
+ RPC method handler that aborts
297
+ """
298
+ def abort(*args, **kwargs):
299
+ context = args[1] if len(args) > 1 else None
300
+ if context:
301
+ context.abort(grpc.StatusCode.UNAUTHENTICATED, message)
302
+
303
+ return grpc.unary_unary_rpc_method_handler(
304
+ abort,
305
+ request_deserializer=lambda x: x,
306
+ response_serializer=lambda x: x,
307
+ )
308
+
309
+
310
+ __all__ = ["ApiKeyAuthInterceptor", "get_current_grpc_user", "get_current_grpc_api_key"]
311
+
312
+
313
+ def get_current_grpc_user():
314
+ """Get current gRPC user from context variables (async-safe)."""
315
+ return _grpc_user_var.get()
316
+
317
+
318
+ def get_current_grpc_api_key():
319
+ """Get current gRPC API key from context variables (async-safe)."""
320
+ return _grpc_api_key_var.get()
@@ -1,7 +1,7 @@
1
1
  """
2
- Logging Interceptor for gRPC.
2
+ Async Logging Interceptor for gRPC.
3
3
 
4
- Provides comprehensive logging for gRPC requests and responses.
4
+ Provides comprehensive logging for async gRPC requests and responses.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -11,13 +11,14 @@ import time
11
11
  from typing import Callable
12
12
 
13
13
  import grpc
14
+ import grpc.aio
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
17
18
 
18
- class LoggingInterceptor(grpc.ServerInterceptor):
19
+ class LoggingInterceptor(grpc.aio.ServerInterceptor):
19
20
  """
20
- gRPC interceptor for request/response logging.
21
+ Async gRPC interceptor for request/response logging.
21
22
 
22
23
  Features:
23
24
  - Logs all incoming requests
@@ -25,6 +26,7 @@ class LoggingInterceptor(grpc.ServerInterceptor):
25
26
  - Logs errors and exceptions
26
27
  - Structured logging with metadata
27
28
  - Performance tracking
29
+ - Async/await support
28
30
 
29
31
  Example:
30
32
  ```python
@@ -40,13 +42,13 @@ class LoggingInterceptor(grpc.ServerInterceptor):
40
42
  [gRPC] METHOD | STATUS | TIME | DETAILS
41
43
  """
42
44
 
43
- def intercept_service(
45
+ async def intercept_service(
44
46
  self,
45
47
  continuation: Callable,
46
48
  handler_call_details: grpc.HandlerCallDetails,
47
49
  ) -> grpc.RpcMethodHandler:
48
50
  """
49
- Intercept gRPC service call for logging.
51
+ Intercept async gRPC service call for logging.
50
52
 
51
53
  Args:
52
54
  continuation: Function to invoke the next interceptor or handler
@@ -62,7 +64,7 @@ class LoggingInterceptor(grpc.ServerInterceptor):
62
64
  logger.info(f"[gRPC] ➡️ {method_name} | peer={peer}")
63
65
 
64
66
  # Get handler and wrap it
65
- handler = continuation(handler_call_details)
67
+ handler = await continuation(handler_call_details)
66
68
 
67
69
  if handler is None:
68
70
  logger.warning(f"[gRPC] ⚠️ {method_name} | No handler found")
@@ -181,19 +183,14 @@ class LoggingInterceptor(grpc.ServerInterceptor):
181
183
  return wrapper
182
184
 
183
185
  def wrap_stream_stream(behavior):
184
- def wrapper(request_iterator, context):
186
+ # All behaviors are async now
187
+ async def async_wrapper(request_iterator, context):
185
188
  start_time = time.time()
186
- in_count = 0
187
189
  out_count = 0
188
190
  try:
189
- # Count input messages
190
- requests = []
191
- for req in request_iterator:
192
- in_count += 1
193
- requests.append(req)
191
+ logger.info(f"[gRPC] 🔄 {method_name} (bidi stream) | peer={peer}")
194
192
 
195
- # Process and count output
196
- for response in behavior(iter(requests), context):
193
+ async for response in behavior(request_iterator, context):
197
194
  out_count += 1
198
195
  yield response
199
196
 
@@ -201,7 +198,7 @@ class LoggingInterceptor(grpc.ServerInterceptor):
201
198
  logger.info(
202
199
  f"[gRPC] ✅ {method_name} (bidi stream) | "
203
200
  f"status=OK | "
204
- f"in={in_count} out={out_count} | "
201
+ f"out={out_count} | "
205
202
  f"time={duration:.2f}ms | "
206
203
  f"peer={peer}"
207
204
  )
@@ -210,14 +207,14 @@ class LoggingInterceptor(grpc.ServerInterceptor):
210
207
  logger.error(
211
208
  f"[gRPC] ❌ {method_name} (bidi stream) | "
212
209
  f"status=ERROR | "
213
- f"in={in_count} out={out_count} | "
210
+ f"out={out_count} | "
214
211
  f"time={duration:.2f}ms | "
215
212
  f"error={type(e).__name__}: {str(e)} | "
216
213
  f"peer={peer}",
217
214
  exc_info=True
218
215
  )
219
216
  raise
220
- return wrapper
217
+ return async_wrapper
221
218
 
222
219
  # Return wrapped handler based on type
223
220
  if handler.unary_unary:
@@ -233,7 +230,7 @@ class LoggingInterceptor(grpc.ServerInterceptor):
233
230
  response_serializer=handler.response_serializer,
234
231
  )
235
232
  elif handler.stream_unary:
236
- return grpc.stream_unary_rpc_method_handler(
233
+ return grpc.stream_stream_rpc_method_handler(
237
234
  wrap_stream_unary(handler.stream_unary),
238
235
  request_deserializer=handler.request_deserializer,
239
236
  response_serializer=handler.response_serializer,
@@ -12,6 +12,7 @@ from collections import defaultdict
12
12
  from typing import Callable
13
13
 
14
14
  import grpc
15
+ import grpc.aio
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
 
@@ -135,9 +136,9 @@ def reset_metrics():
135
136
  _metrics.reset()
136
137
 
137
138
 
138
- class MetricsInterceptor(grpc.ServerInterceptor):
139
+ class MetricsInterceptor(grpc.aio.ServerInterceptor):
139
140
  """
140
- gRPC interceptor for metrics collection.
141
+ gRPC interceptor for metrics collection (async).
141
142
 
142
143
  Features:
143
144
  - Tracks request counts
@@ -170,13 +171,13 @@ class MetricsInterceptor(grpc.ServerInterceptor):
170
171
  """Initialize metrics interceptor."""
171
172
  self.collector = _metrics
172
173
 
173
- def intercept_service(
174
+ async def intercept_service(
174
175
  self,
175
176
  continuation: Callable,
176
177
  handler_call_details: grpc.HandlerCallDetails,
177
178
  ) -> grpc.RpcMethodHandler:
178
179
  """
179
- Intercept gRPC service call for metrics collection.
180
+ Intercept gRPC service call for metrics collection (async).
180
181
 
181
182
  Args:
182
183
  continuation: Function to invoke the next interceptor or handler
@@ -190,8 +191,8 @@ class MetricsInterceptor(grpc.ServerInterceptor):
190
191
  # Record request
191
192
  self.collector.record_request(method_name)
192
193
 
193
- # Get handler and wrap it
194
- handler = continuation(handler_call_details)
194
+ # Get handler and wrap it (await for async)
195
+ handler = await continuation(handler_call_details)
195
196
 
196
197
  if handler is None:
197
198
  return None
@@ -215,10 +216,10 @@ class MetricsInterceptor(grpc.ServerInterceptor):
215
216
  Wrapped RPC method handler
216
217
  """
217
218
  def wrap_unary_unary(behavior):
218
- def wrapper(request, context):
219
+ async def wrapper(request, context):
219
220
  start_time = time.time()
220
221
  try:
221
- response = behavior(request, context)
222
+ response = await behavior(request, context)
222
223
  duration_ms = (time.time() - start_time) * 1000
223
224
  self.collector.record_response_time(method_name, duration_ms)
224
225
  return response
@@ -230,10 +231,10 @@ class MetricsInterceptor(grpc.ServerInterceptor):
230
231
  return wrapper
231
232
 
232
233
  def wrap_unary_stream(behavior):
233
- def wrapper(request, context):
234
+ async def wrapper(request, context):
234
235
  start_time = time.time()
235
236
  try:
236
- for response in behavior(request, context):
237
+ async for response in behavior(request, context):
237
238
  yield response
238
239
  duration_ms = (time.time() - start_time) * 1000
239
240
  self.collector.record_response_time(method_name, duration_ms)
@@ -245,10 +246,10 @@ class MetricsInterceptor(grpc.ServerInterceptor):
245
246
  return wrapper
246
247
 
247
248
  def wrap_stream_unary(behavior):
248
- def wrapper(request_iterator, context):
249
+ async def wrapper(request_iterator, context):
249
250
  start_time = time.time()
250
251
  try:
251
- response = behavior(request_iterator, context)
252
+ response = await behavior(request_iterator, context)
252
253
  duration_ms = (time.time() - start_time) * 1000
253
254
  self.collector.record_response_time(method_name, duration_ms)
254
255
  return response
@@ -260,10 +261,10 @@ class MetricsInterceptor(grpc.ServerInterceptor):
260
261
  return wrapper
261
262
 
262
263
  def wrap_stream_stream(behavior):
263
- def wrapper(request_iterator, context):
264
+ async def wrapper(request_iterator, context):
264
265
  start_time = time.time()
265
266
  try:
266
- for response in behavior(request_iterator, context):
267
+ async for response in behavior(request_iterator, context):
267
268
  yield response
268
269
  duration_ms = (time.time() - start_time) * 1000
269
270
  self.collector.record_response_time(method_name, duration_ms)