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.

Files changed (84) hide show
  1. django_cfg/__init__.py +8 -4
  2. django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
  3. django_cfg/apps/grpc/__init__.py +9 -0
  4. django_cfg/apps/grpc/admin/__init__.py +11 -0
  5. django_cfg/apps/grpc/admin/config.py +89 -0
  6. django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
  7. django_cfg/apps/grpc/apps.py +28 -0
  8. django_cfg/apps/grpc/auth/__init__.py +9 -0
  9. django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
  10. django_cfg/apps/grpc/interceptors/__init__.py +19 -0
  11. django_cfg/apps/grpc/interceptors/errors.py +241 -0
  12. django_cfg/apps/grpc/interceptors/logging.py +270 -0
  13. django_cfg/apps/grpc/interceptors/metrics.py +306 -0
  14. django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
  15. django_cfg/apps/grpc/management/__init__.py +1 -0
  16. django_cfg/apps/grpc/management/commands/__init__.py +0 -0
  17. django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
  18. django_cfg/apps/grpc/managers/__init__.py +10 -0
  19. django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
  20. django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
  21. django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
  22. django_cfg/apps/grpc/migrations/__init__.py +0 -0
  23. django_cfg/apps/grpc/models/__init__.py +9 -0
  24. django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
  25. django_cfg/apps/grpc/serializers/__init__.py +23 -0
  26. django_cfg/apps/grpc/serializers/health.py +18 -0
  27. django_cfg/apps/grpc/serializers/requests.py +18 -0
  28. django_cfg/apps/grpc/serializers/services.py +50 -0
  29. django_cfg/apps/grpc/serializers/stats.py +22 -0
  30. django_cfg/apps/grpc/services/__init__.py +16 -0
  31. django_cfg/apps/grpc/services/base.py +375 -0
  32. django_cfg/apps/grpc/services/discovery.py +415 -0
  33. django_cfg/apps/grpc/urls.py +23 -0
  34. django_cfg/apps/grpc/utils/__init__.py +13 -0
  35. django_cfg/apps/grpc/utils/proto_gen.py +423 -0
  36. django_cfg/apps/grpc/views/__init__.py +9 -0
  37. django_cfg/apps/grpc/views/monitoring.py +497 -0
  38. django_cfg/apps/maintenance/admin/api_key_admin.py +7 -8
  39. django_cfg/apps/maintenance/admin/site_admin.py +5 -4
  40. django_cfg/apps/payments/admin/balance_admin.py +26 -36
  41. django_cfg/apps/payments/admin/payment_admin.py +65 -85
  42. django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
  43. django_cfg/apps/tasks/admin/task_log.py +20 -47
  44. django_cfg/apps/urls.py +7 -1
  45. django_cfg/config.py +106 -0
  46. django_cfg/core/base/config_model.py +6 -0
  47. django_cfg/core/builders/apps_builder.py +3 -0
  48. django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
  49. django_cfg/core/generation/orchestrator.py +10 -0
  50. django_cfg/models/api/grpc/__init__.py +59 -0
  51. django_cfg/models/api/grpc/config.py +364 -0
  52. django_cfg/modules/base.py +15 -0
  53. django_cfg/modules/django_admin/__init__.py +2 -0
  54. django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
  55. django_cfg/modules/django_admin/config/__init__.py +2 -0
  56. django_cfg/modules/django_admin/config/field_config.py +24 -0
  57. django_cfg/modules/django_admin/utils/__init__.py +41 -3
  58. django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
  59. django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
  60. django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
  61. django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
  62. django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
  63. django_cfg/modules/django_admin/utils/html/badges.py +47 -0
  64. django_cfg/modules/django_admin/utils/html/base.py +167 -0
  65. django_cfg/modules/django_admin/utils/html/code.py +87 -0
  66. django_cfg/modules/django_admin/utils/html/composition.py +198 -0
  67. django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
  68. django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
  69. django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
  70. django_cfg/modules/django_admin/utils/html/progress.py +127 -0
  71. django_cfg/modules/django_admin/utils/html_builder.py +55 -408
  72. django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
  73. django_cfg/modules/django_admin/widgets/registry.py +42 -0
  74. django_cfg/modules/django_unfold/navigation.py +28 -0
  75. django_cfg/pyproject.toml +3 -5
  76. django_cfg/registry/modules.py +6 -0
  77. {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/METADATA +10 -1
  78. {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/RECORD +83 -34
  79. django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
  80. /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
  81. /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
  82. {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/WHEEL +0 -0
  83. {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/entry_points.txt +0 -0
  84. {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"]