django-cfg 1.4.119__py3-none-any.whl → 1.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +8 -4
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
- django_cfg/apps/grpc/__init__.py +9 -0
- django_cfg/apps/grpc/admin/__init__.py +11 -0
- django_cfg/apps/grpc/admin/config.py +89 -0
- django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
- django_cfg/apps/grpc/apps.py +28 -0
- django_cfg/apps/grpc/auth/__init__.py +9 -0
- django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
- django_cfg/apps/grpc/interceptors/__init__.py +19 -0
- django_cfg/apps/grpc/interceptors/errors.py +241 -0
- django_cfg/apps/grpc/interceptors/logging.py +270 -0
- django_cfg/apps/grpc/interceptors/metrics.py +306 -0
- django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
- django_cfg/apps/grpc/management/__init__.py +1 -0
- django_cfg/apps/grpc/management/commands/__init__.py +0 -0
- django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
- django_cfg/apps/grpc/managers/__init__.py +10 -0
- django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
- django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
- django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
- django_cfg/apps/grpc/migrations/__init__.py +0 -0
- django_cfg/apps/grpc/models/__init__.py +9 -0
- django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
- django_cfg/apps/grpc/serializers/__init__.py +23 -0
- django_cfg/apps/grpc/serializers/health.py +18 -0
- django_cfg/apps/grpc/serializers/requests.py +18 -0
- django_cfg/apps/grpc/serializers/services.py +50 -0
- django_cfg/apps/grpc/serializers/stats.py +22 -0
- django_cfg/apps/grpc/services/__init__.py +16 -0
- django_cfg/apps/grpc/services/base.py +375 -0
- django_cfg/apps/grpc/services/discovery.py +415 -0
- django_cfg/apps/grpc/urls.py +23 -0
- django_cfg/apps/grpc/utils/__init__.py +13 -0
- django_cfg/apps/grpc/utils/proto_gen.py +423 -0
- django_cfg/apps/grpc/views/__init__.py +9 -0
- django_cfg/apps/grpc/views/monitoring.py +497 -0
- django_cfg/apps/maintenance/admin/api_key_admin.py +7 -8
- django_cfg/apps/maintenance/admin/site_admin.py +5 -4
- django_cfg/apps/payments/admin/balance_admin.py +26 -36
- django_cfg/apps/payments/admin/payment_admin.py +65 -85
- django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
- django_cfg/apps/tasks/admin/task_log.py +20 -47
- django_cfg/apps/urls.py +7 -1
- django_cfg/config.py +106 -0
- django_cfg/core/base/config_model.py +6 -0
- django_cfg/core/builders/apps_builder.py +3 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
- django_cfg/core/generation/orchestrator.py +10 -0
- django_cfg/models/api/grpc/__init__.py +59 -0
- django_cfg/models/api/grpc/config.py +364 -0
- django_cfg/modules/base.py +15 -0
- django_cfg/modules/django_admin/__init__.py +2 -0
- django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
- django_cfg/modules/django_admin/config/__init__.py +2 -0
- django_cfg/modules/django_admin/config/field_config.py +24 -0
- django_cfg/modules/django_admin/utils/__init__.py +41 -3
- django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
- django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
- django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
- django_cfg/modules/django_admin/utils/html/badges.py +47 -0
- django_cfg/modules/django_admin/utils/html/base.py +167 -0
- django_cfg/modules/django_admin/utils/html/code.py +87 -0
- django_cfg/modules/django_admin/utils/html/composition.py +198 -0
- django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
- django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
- django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
- django_cfg/modules/django_admin/utils/html/progress.py +127 -0
- django_cfg/modules/django_admin/utils/html_builder.py +55 -408
- django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
- django_cfg/modules/django_admin/widgets/registry.py +42 -0
- django_cfg/modules/django_unfold/navigation.py +28 -0
- django_cfg/pyproject.toml +3 -5
- django_cfg/registry/modules.py +6 -0
- {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/METADATA +10 -1
- {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/RECORD +83 -34
- django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
- /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
- /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
- {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JWT Authentication Interceptor for gRPC.
|
|
3
|
+
|
|
4
|
+
Handles JWT token verification and Django user authentication for gRPC requests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Callable, Optional
|
|
9
|
+
|
|
10
|
+
import grpc
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
from django.contrib.auth import get_user_model
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
User = get_user_model()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class JWTAuthInterceptor(grpc.ServerInterceptor):
|
|
20
|
+
"""
|
|
21
|
+
gRPC interceptor for JWT authentication.
|
|
22
|
+
|
|
23
|
+
Features:
|
|
24
|
+
- Extracts JWT token from metadata
|
|
25
|
+
- Verifies token signature and expiration
|
|
26
|
+
- Loads Django user from token
|
|
27
|
+
- Sets user on request context
|
|
28
|
+
- Supports public methods whitelist
|
|
29
|
+
- Handles authentication errors gracefully
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
```python
|
|
33
|
+
# In Django settings (auto-configured by django-cfg)
|
|
34
|
+
GRPC_FRAMEWORK = {
|
|
35
|
+
"SERVER_INTERCEPTORS": [
|
|
36
|
+
"django_cfg.apps.grpc.auth.JWTAuthInterceptor",
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Token Format:
|
|
42
|
+
Authorization: Bearer <jwt_token>
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self):
|
|
46
|
+
"""Initialize JWT authentication interceptor."""
|
|
47
|
+
self.grpc_auth_config = getattr(settings, "GRPC_AUTH", {})
|
|
48
|
+
self.enabled = self.grpc_auth_config.get("enabled", True)
|
|
49
|
+
self.require_auth = self.grpc_auth_config.get("require_auth", True)
|
|
50
|
+
self.token_header = self.grpc_auth_config.get("token_header", "authorization")
|
|
51
|
+
self.token_prefix = self.grpc_auth_config.get("token_prefix", "Bearer")
|
|
52
|
+
self.public_methods = self.grpc_auth_config.get("public_methods", [
|
|
53
|
+
"/grpc.health.v1.Health/Check",
|
|
54
|
+
"/grpc.health.v1.Health/Watch",
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
# JWT settings
|
|
58
|
+
self.jwt_secret_key = self.grpc_auth_config.get("jwt_secret_key") or settings.SECRET_KEY
|
|
59
|
+
self.jwt_algorithm = self.grpc_auth_config.get("jwt_algorithm", "HS256")
|
|
60
|
+
self.jwt_verify_exp = self.grpc_auth_config.get("jwt_verify_exp", True)
|
|
61
|
+
self.jwt_leeway = self.grpc_auth_config.get("jwt_leeway", 0)
|
|
62
|
+
|
|
63
|
+
def intercept_service(self, continuation: Callable, handler_call_details: grpc.HandlerCallDetails) -> grpc.RpcMethodHandler:
|
|
64
|
+
"""
|
|
65
|
+
Intercept gRPC service call for authentication.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
continuation: Function to invoke the next interceptor or handler
|
|
69
|
+
handler_call_details: Details about the RPC call
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
RPC method handler (possibly wrapped with auth)
|
|
73
|
+
"""
|
|
74
|
+
# Skip if auth is disabled
|
|
75
|
+
if not self.enabled:
|
|
76
|
+
return continuation(handler_call_details)
|
|
77
|
+
|
|
78
|
+
# Check if method is public
|
|
79
|
+
method_name = handler_call_details.method
|
|
80
|
+
if method_name in self.public_methods:
|
|
81
|
+
logger.debug(f"Public method accessed: {method_name}")
|
|
82
|
+
return continuation(handler_call_details)
|
|
83
|
+
|
|
84
|
+
# Extract token from metadata
|
|
85
|
+
token = self._extract_token(handler_call_details.invocation_metadata)
|
|
86
|
+
|
|
87
|
+
# If no token and auth is required, abort
|
|
88
|
+
if not token:
|
|
89
|
+
if self.require_auth:
|
|
90
|
+
logger.warning(f"Missing authentication token for {method_name}")
|
|
91
|
+
return self._abort_unauthenticated(
|
|
92
|
+
"Authentication token is required"
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
# Allow anonymous access
|
|
96
|
+
logger.debug(f"No token provided for {method_name}, allowing anonymous access")
|
|
97
|
+
return continuation(handler_call_details)
|
|
98
|
+
|
|
99
|
+
# Verify token and get user
|
|
100
|
+
user = self._verify_token(token)
|
|
101
|
+
|
|
102
|
+
if not user:
|
|
103
|
+
if self.require_auth:
|
|
104
|
+
logger.warning(f"Invalid authentication token for {method_name}")
|
|
105
|
+
return self._abort_unauthenticated(
|
|
106
|
+
"Invalid or expired authentication token"
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
# Allow anonymous access even with invalid token
|
|
110
|
+
return continuation(handler_call_details)
|
|
111
|
+
|
|
112
|
+
# Add user to context and continue
|
|
113
|
+
logger.debug(f"Authenticated user {user.id} for {method_name}")
|
|
114
|
+
return self._continue_with_user(continuation, handler_call_details, user)
|
|
115
|
+
|
|
116
|
+
def _extract_token(self, metadata: tuple) -> Optional[str]:
|
|
117
|
+
"""
|
|
118
|
+
Extract JWT token from gRPC metadata.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
metadata: gRPC invocation metadata
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
JWT token string or None
|
|
125
|
+
"""
|
|
126
|
+
if not metadata:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# Convert metadata to dict
|
|
130
|
+
metadata_dict = dict(metadata)
|
|
131
|
+
|
|
132
|
+
# Get authorization header (case-insensitive)
|
|
133
|
+
auth_header = None
|
|
134
|
+
for key, value in metadata_dict.items():
|
|
135
|
+
if key.lower() == self.token_header.lower():
|
|
136
|
+
auth_header = value
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
if not auth_header:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
# Extract token from "Bearer <token>" format
|
|
143
|
+
if auth_header.startswith(f"{self.token_prefix} "):
|
|
144
|
+
return auth_header[len(self.token_prefix) + 1:]
|
|
145
|
+
elif self.token_prefix == "":
|
|
146
|
+
# No prefix expected
|
|
147
|
+
return auth_header
|
|
148
|
+
else:
|
|
149
|
+
# Invalid format
|
|
150
|
+
logger.warning(f"Invalid authorization header format: {auth_header[:20]}...")
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
def _verify_token(self, token: str) -> Optional[User]:
|
|
154
|
+
"""
|
|
155
|
+
Verify JWT token and return user.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
token: JWT token string
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Django User instance or None
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
import jwt
|
|
165
|
+
|
|
166
|
+
# Decode JWT token
|
|
167
|
+
payload = jwt.decode(
|
|
168
|
+
token,
|
|
169
|
+
self.jwt_secret_key,
|
|
170
|
+
algorithms=[self.jwt_algorithm],
|
|
171
|
+
options={
|
|
172
|
+
"verify_exp": self.jwt_verify_exp,
|
|
173
|
+
},
|
|
174
|
+
leeway=self.jwt_leeway,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Extract user ID from payload
|
|
178
|
+
user_id = payload.get("user_id")
|
|
179
|
+
if not user_id:
|
|
180
|
+
logger.warning("Token missing user_id claim")
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
# Load user from database
|
|
184
|
+
try:
|
|
185
|
+
user = User.objects.get(pk=user_id)
|
|
186
|
+
if not user.is_active:
|
|
187
|
+
logger.warning(f"Inactive user {user_id} attempted to authenticate")
|
|
188
|
+
return None
|
|
189
|
+
return user
|
|
190
|
+
except User.DoesNotExist:
|
|
191
|
+
logger.warning(f"User {user_id} from token does not exist")
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
except jwt.ExpiredSignatureError:
|
|
195
|
+
logger.warning("JWT token has expired")
|
|
196
|
+
return None
|
|
197
|
+
except jwt.InvalidTokenError as e:
|
|
198
|
+
logger.warning(f"Invalid JWT token: {e}")
|
|
199
|
+
return None
|
|
200
|
+
except ImportError:
|
|
201
|
+
logger.error("PyJWT library not installed. Install with: pip install PyJWT")
|
|
202
|
+
return None
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.error(f"Unexpected error verifying token: {e}")
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
def _continue_with_user(
|
|
208
|
+
self,
|
|
209
|
+
continuation: Callable,
|
|
210
|
+
handler_call_details: grpc.HandlerCallDetails,
|
|
211
|
+
user: User,
|
|
212
|
+
) -> grpc.RpcMethodHandler:
|
|
213
|
+
"""
|
|
214
|
+
Continue RPC with authenticated user in context.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
continuation: Function to invoke next interceptor or handler
|
|
218
|
+
handler_call_details: Details about the RPC call
|
|
219
|
+
user: Authenticated Django user
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
RPC method handler with user context
|
|
223
|
+
"""
|
|
224
|
+
# Get the handler
|
|
225
|
+
handler = continuation(handler_call_details)
|
|
226
|
+
|
|
227
|
+
if handler is None:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
# Wrap the handler to inject user into context
|
|
231
|
+
def wrapped_unary_unary(request, context):
|
|
232
|
+
# Set user on context for access in service methods
|
|
233
|
+
context.user = user
|
|
234
|
+
return handler.unary_unary(request, context)
|
|
235
|
+
|
|
236
|
+
def wrapped_unary_stream(request, context):
|
|
237
|
+
context.user = user
|
|
238
|
+
return handler.unary_stream(request, context)
|
|
239
|
+
|
|
240
|
+
def wrapped_stream_unary(request_iterator, context):
|
|
241
|
+
context.user = user
|
|
242
|
+
return handler.stream_unary(request_iterator, context)
|
|
243
|
+
|
|
244
|
+
def wrapped_stream_stream(request_iterator, context):
|
|
245
|
+
context.user = user
|
|
246
|
+
return handler.stream_stream(request_iterator, context)
|
|
247
|
+
|
|
248
|
+
# Return wrapped handler based on type
|
|
249
|
+
return grpc.unary_unary_rpc_method_handler(
|
|
250
|
+
wrapped_unary_unary,
|
|
251
|
+
request_deserializer=handler.request_deserializer,
|
|
252
|
+
response_serializer=handler.response_serializer,
|
|
253
|
+
) if handler.unary_unary else (
|
|
254
|
+
grpc.unary_stream_rpc_method_handler(
|
|
255
|
+
wrapped_unary_stream,
|
|
256
|
+
request_deserializer=handler.request_deserializer,
|
|
257
|
+
response_serializer=handler.response_serializer,
|
|
258
|
+
) if handler.unary_stream else (
|
|
259
|
+
grpc.stream_unary_rpc_method_handler(
|
|
260
|
+
wrapped_stream_unary,
|
|
261
|
+
request_deserializer=handler.request_deserializer,
|
|
262
|
+
response_serializer=handler.response_serializer,
|
|
263
|
+
) if handler.stream_unary else (
|
|
264
|
+
grpc.stream_stream_rpc_method_handler(
|
|
265
|
+
wrapped_stream_stream,
|
|
266
|
+
request_deserializer=handler.request_deserializer,
|
|
267
|
+
response_serializer=handler.response_serializer,
|
|
268
|
+
) if handler.stream_stream else None
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _abort_unauthenticated(self, message: str) -> grpc.RpcMethodHandler:
|
|
274
|
+
"""
|
|
275
|
+
Return handler that aborts with UNAUTHENTICATED status.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
message: Error message
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
RPC method handler that aborts
|
|
282
|
+
"""
|
|
283
|
+
def abort(*args, **kwargs):
|
|
284
|
+
context = args[1] if len(args) > 1 else None
|
|
285
|
+
if context:
|
|
286
|
+
context.abort(grpc.StatusCode.UNAUTHENTICATED, message)
|
|
287
|
+
|
|
288
|
+
return grpc.unary_unary_rpc_method_handler(
|
|
289
|
+
abort,
|
|
290
|
+
request_deserializer=lambda x: x,
|
|
291
|
+
response_serializer=lambda x: x,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
__all__ = ["JWTAuthInterceptor"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gRPC interceptors for logging, metrics, and error handling.
|
|
3
|
+
|
|
4
|
+
Provides production-ready interceptors for gRPC services.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .errors import ErrorHandlingInterceptor
|
|
8
|
+
from .logging import LoggingInterceptor
|
|
9
|
+
from .metrics import MetricsInterceptor, get_metrics, reset_metrics
|
|
10
|
+
from .request_logger import RequestLoggerInterceptor
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"LoggingInterceptor",
|
|
14
|
+
"MetricsInterceptor",
|
|
15
|
+
"ErrorHandlingInterceptor",
|
|
16
|
+
"RequestLoggerInterceptor",
|
|
17
|
+
"get_metrics",
|
|
18
|
+
"reset_metrics",
|
|
19
|
+
]
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error Handling Interceptor for gRPC.
|
|
3
|
+
|
|
4
|
+
Catches exceptions and converts them to appropriate gRPC errors.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
import grpc
|
|
13
|
+
from django.core.exceptions import (
|
|
14
|
+
ObjectDoesNotExist,
|
|
15
|
+
PermissionDenied,
|
|
16
|
+
ValidationError as DjangoValidationError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ErrorHandlingInterceptor(grpc.ServerInterceptor):
|
|
23
|
+
"""
|
|
24
|
+
gRPC interceptor for error handling.
|
|
25
|
+
|
|
26
|
+
Features:
|
|
27
|
+
- Catches Python exceptions
|
|
28
|
+
- Converts to appropriate gRPC status codes
|
|
29
|
+
- Logs errors with context
|
|
30
|
+
- Provides user-friendly error messages
|
|
31
|
+
- Supports custom error mappings
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
```python
|
|
35
|
+
# In Django settings
|
|
36
|
+
GRPC_FRAMEWORK = {
|
|
37
|
+
"SERVER_INTERCEPTORS": [
|
|
38
|
+
"django_cfg.apps.grpc.interceptors.ErrorHandlingInterceptor",
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Error Mapping:
|
|
44
|
+
- ValidationError → INVALID_ARGUMENT
|
|
45
|
+
- ObjectDoesNotExist → NOT_FOUND
|
|
46
|
+
- PermissionDenied → PERMISSION_DENIED
|
|
47
|
+
- NotImplementedError → UNIMPLEMENTED
|
|
48
|
+
- TimeoutError → DEADLINE_EXCEEDED
|
|
49
|
+
- Exception → INTERNAL
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self):
|
|
53
|
+
"""Initialize error handling interceptor."""
|
|
54
|
+
self.error_mappings = {
|
|
55
|
+
# Django exceptions
|
|
56
|
+
DjangoValidationError: (
|
|
57
|
+
grpc.StatusCode.INVALID_ARGUMENT,
|
|
58
|
+
"Validation error: {message}"
|
|
59
|
+
),
|
|
60
|
+
ObjectDoesNotExist: (
|
|
61
|
+
grpc.StatusCode.NOT_FOUND,
|
|
62
|
+
"Object not found: {message}"
|
|
63
|
+
),
|
|
64
|
+
PermissionDenied: (
|
|
65
|
+
grpc.StatusCode.PERMISSION_DENIED,
|
|
66
|
+
"Permission denied: {message}"
|
|
67
|
+
),
|
|
68
|
+
# Python built-in exceptions
|
|
69
|
+
ValueError: (
|
|
70
|
+
grpc.StatusCode.INVALID_ARGUMENT,
|
|
71
|
+
"Invalid value: {message}"
|
|
72
|
+
),
|
|
73
|
+
KeyError: (
|
|
74
|
+
grpc.StatusCode.INVALID_ARGUMENT,
|
|
75
|
+
"Missing required field: {message}"
|
|
76
|
+
),
|
|
77
|
+
NotImplementedError: (
|
|
78
|
+
grpc.StatusCode.UNIMPLEMENTED,
|
|
79
|
+
"Not implemented: {message}"
|
|
80
|
+
),
|
|
81
|
+
TimeoutError: (
|
|
82
|
+
grpc.StatusCode.DEADLINE_EXCEEDED,
|
|
83
|
+
"Operation timed out: {message}"
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def intercept_service(
|
|
88
|
+
self,
|
|
89
|
+
continuation: Callable,
|
|
90
|
+
handler_call_details: grpc.HandlerCallDetails,
|
|
91
|
+
) -> grpc.RpcMethodHandler:
|
|
92
|
+
"""
|
|
93
|
+
Intercept gRPC service call for error handling.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
continuation: Function to invoke the next interceptor or handler
|
|
97
|
+
handler_call_details: Details about the RPC call
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
RPC method handler with error handling
|
|
101
|
+
"""
|
|
102
|
+
method_name = handler_call_details.method
|
|
103
|
+
|
|
104
|
+
# Get handler and wrap it
|
|
105
|
+
handler = continuation(handler_call_details)
|
|
106
|
+
|
|
107
|
+
if handler is None:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
# Wrap handler methods to catch errors
|
|
111
|
+
return self._wrap_handler(handler, method_name)
|
|
112
|
+
|
|
113
|
+
def _wrap_handler(
|
|
114
|
+
self,
|
|
115
|
+
handler: grpc.RpcMethodHandler,
|
|
116
|
+
method_name: str,
|
|
117
|
+
) -> grpc.RpcMethodHandler:
|
|
118
|
+
"""
|
|
119
|
+
Wrap handler to catch and convert exceptions.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
handler: Original RPC method handler
|
|
123
|
+
method_name: gRPC method name
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Wrapped RPC method handler
|
|
127
|
+
"""
|
|
128
|
+
def wrap_unary_unary(behavior):
|
|
129
|
+
def wrapper(request, context):
|
|
130
|
+
try:
|
|
131
|
+
return behavior(request, context)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
self._handle_error(e, context, method_name)
|
|
134
|
+
return wrapper
|
|
135
|
+
|
|
136
|
+
def wrap_unary_stream(behavior):
|
|
137
|
+
def wrapper(request, context):
|
|
138
|
+
try:
|
|
139
|
+
for response in behavior(request, context):
|
|
140
|
+
yield response
|
|
141
|
+
except Exception as e:
|
|
142
|
+
self._handle_error(e, context, method_name)
|
|
143
|
+
return wrapper
|
|
144
|
+
|
|
145
|
+
def wrap_stream_unary(behavior):
|
|
146
|
+
def wrapper(request_iterator, context):
|
|
147
|
+
try:
|
|
148
|
+
return behavior(request_iterator, context)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
self._handle_error(e, context, method_name)
|
|
151
|
+
return wrapper
|
|
152
|
+
|
|
153
|
+
def wrap_stream_stream(behavior):
|
|
154
|
+
def wrapper(request_iterator, context):
|
|
155
|
+
try:
|
|
156
|
+
for response in behavior(request_iterator, context):
|
|
157
|
+
yield response
|
|
158
|
+
except Exception as e:
|
|
159
|
+
self._handle_error(e, context, method_name)
|
|
160
|
+
return wrapper
|
|
161
|
+
|
|
162
|
+
# Return wrapped handler based on type
|
|
163
|
+
if handler.unary_unary:
|
|
164
|
+
return grpc.unary_unary_rpc_method_handler(
|
|
165
|
+
wrap_unary_unary(handler.unary_unary),
|
|
166
|
+
request_deserializer=handler.request_deserializer,
|
|
167
|
+
response_serializer=handler.response_serializer,
|
|
168
|
+
)
|
|
169
|
+
elif handler.unary_stream:
|
|
170
|
+
return grpc.unary_stream_rpc_method_handler(
|
|
171
|
+
wrap_unary_stream(handler.unary_stream),
|
|
172
|
+
request_deserializer=handler.request_deserializer,
|
|
173
|
+
response_serializer=handler.response_serializer,
|
|
174
|
+
)
|
|
175
|
+
elif handler.stream_unary:
|
|
176
|
+
return grpc.stream_unary_rpc_method_handler(
|
|
177
|
+
wrap_stream_unary(handler.stream_unary),
|
|
178
|
+
request_deserializer=handler.request_deserializer,
|
|
179
|
+
response_serializer=handler.response_serializer,
|
|
180
|
+
)
|
|
181
|
+
elif handler.stream_stream:
|
|
182
|
+
return grpc.stream_stream_rpc_method_handler(
|
|
183
|
+
wrap_stream_stream(handler.stream_stream),
|
|
184
|
+
request_deserializer=handler.request_deserializer,
|
|
185
|
+
response_serializer=handler.response_serializer,
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
return handler
|
|
189
|
+
|
|
190
|
+
def _handle_error(self, error: Exception, context: grpc.ServicerContext, method_name: str):
|
|
191
|
+
"""
|
|
192
|
+
Handle exception and abort with appropriate gRPC status.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
error: The caught exception
|
|
196
|
+
context: gRPC servicer context
|
|
197
|
+
method_name: Name of the gRPC method
|
|
198
|
+
"""
|
|
199
|
+
# Check if it's already a gRPC error
|
|
200
|
+
if isinstance(error, grpc.RpcError):
|
|
201
|
+
# Re-raise gRPC errors as-is
|
|
202
|
+
raise error
|
|
203
|
+
|
|
204
|
+
# Get error mapping
|
|
205
|
+
error_type = type(error)
|
|
206
|
+
status_code = grpc.StatusCode.INTERNAL
|
|
207
|
+
message_template = "Internal server error: {message}"
|
|
208
|
+
|
|
209
|
+
# Find matching error mapping
|
|
210
|
+
for exc_type, (code, template) in self.error_mappings.items():
|
|
211
|
+
if isinstance(error, exc_type):
|
|
212
|
+
status_code = code
|
|
213
|
+
message_template = template
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
# Format error message
|
|
217
|
+
error_message = str(error) or error_type.__name__
|
|
218
|
+
formatted_message = message_template.format(message=error_message)
|
|
219
|
+
|
|
220
|
+
# Log error
|
|
221
|
+
if status_code == grpc.StatusCode.INTERNAL:
|
|
222
|
+
# Internal errors should be logged with full traceback
|
|
223
|
+
logger.error(
|
|
224
|
+
f"[gRPC Error] {method_name} | "
|
|
225
|
+
f"status={status_code.name} | "
|
|
226
|
+
f"error={error_type.__name__}: {error_message}",
|
|
227
|
+
exc_info=True
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
# Expected errors can be logged at warning level
|
|
231
|
+
logger.warning(
|
|
232
|
+
f"[gRPC Error] {method_name} | "
|
|
233
|
+
f"status={status_code.name} | "
|
|
234
|
+
f"error={error_type.__name__}: {error_message}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Abort with gRPC error
|
|
238
|
+
context.abort(status_code, formatted_message)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
__all__ = ["ErrorHandlingInterceptor"]
|