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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/commands/serializers.py +152 -0
- django_cfg/apps/api/commands/views.py +32 -0
- django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
- django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
- django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
- django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
- django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
- django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
- django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
- django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +5 -5
- django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
- django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
- django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
- django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
- django_cfg/apps/integrations/grpc/admin/config.py +113 -9
- django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
- django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
- django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
- django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
- django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
- django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
- django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +130 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +171 -96
- django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
- django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
- django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
- django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
- django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
- django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
- django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
- django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
- django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
- django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
- django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
- django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
- django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
- django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
- django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
- django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
- django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
- django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
- django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
- django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
- django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
- django_cfg/apps/integrations/grpc/urls.py +8 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
- django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +177 -0
- django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
- django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
- django_cfg/apps/integrations/grpc/views/charts.py +21 -14
- django_cfg/apps/integrations/grpc/views/config.py +8 -6
- django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
- django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
- django_cfg/apps/integrations/grpc/views/services.py +30 -21
- django_cfg/apps/integrations/grpc/views/testing.py +45 -43
- django_cfg/apps/integrations/rq/views/jobs.py +19 -9
- django_cfg/apps/integrations/rq/views/schedule.py +7 -3
- django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
- django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
- django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
- django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
- django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
- django_cfg/config.py +33 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
- django_cfg/management/commands/check_endpoints.py +2 -2
- django_cfg/management/commands/check_settings.py +3 -10
- django_cfg/management/commands/clear_constance.py +3 -10
- django_cfg/management/commands/create_token.py +4 -11
- django_cfg/management/commands/list_urls.py +4 -10
- django_cfg/management/commands/migrate_all.py +18 -12
- django_cfg/management/commands/migrator.py +4 -11
- django_cfg/management/commands/script.py +4 -10
- django_cfg/management/commands/show_config.py +8 -16
- django_cfg/management/commands/show_urls.py +5 -11
- django_cfg/management/commands/superuser.py +4 -11
- django_cfg/management/commands/tree.py +5 -10
- django_cfg/management/utils/README.md +402 -0
- django_cfg/management/utils/__init__.py +29 -0
- django_cfg/management/utils/mixins.py +176 -0
- django_cfg/middleware/pagination.py +53 -54
- django_cfg/models/api/grpc/__init__.py +15 -21
- django_cfg/models/api/grpc/config.py +155 -73
- django_cfg/models/ngrok/config.py +7 -6
- django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
- django_cfg/modules/django_client/core/parser/base.py +114 -30
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
- django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
- django_cfg/modules/django_email/management/commands/test_email.py +4 -10
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
- django_cfg/modules/django_unfold/navigation.py +6 -18
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/modules.py +1 -4
- django_cfg/requirements.txt +52 -0
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/METADATA +1 -1
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/RECORD +118 -97
- django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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
|
|
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.
|
|
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)
|