django-cfg 1.4.120__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 (80) 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/base/pydantic_admin.py +2 -2
  54. django_cfg/modules/django_admin/utils/__init__.py +41 -3
  55. django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
  56. django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
  57. django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
  58. django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
  59. django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
  60. django_cfg/modules/django_admin/utils/html/badges.py +47 -0
  61. django_cfg/modules/django_admin/utils/html/base.py +167 -0
  62. django_cfg/modules/django_admin/utils/html/code.py +87 -0
  63. django_cfg/modules/django_admin/utils/html/composition.py +198 -0
  64. django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
  65. django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
  66. django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
  67. django_cfg/modules/django_admin/utils/html/progress.py +127 -0
  68. django_cfg/modules/django_admin/utils/html_builder.py +55 -408
  69. django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
  70. django_cfg/modules/django_unfold/navigation.py +28 -0
  71. django_cfg/pyproject.toml +3 -5
  72. django_cfg/registry/modules.py +6 -0
  73. {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/METADATA +10 -1
  74. {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/RECORD +79 -30
  75. django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
  76. /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
  77. /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
  78. {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/WHEEL +0 -0
  79. {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/entry_points.txt +0 -0
  80. {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,11 +32,11 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.4.120"
35
+ __version__ = "1.5.1"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
39
- from .config import LIB_NAME
39
+ from .config import LIB_NAME, is_feature_available, require_feature, register_feature
40
40
  from .registry import DJANGO_CFG_REGISTRY
41
41
 
42
42
  # Get author from library config
@@ -55,5 +55,9 @@ def __getattr__(name: str):
55
55
  raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
56
56
 
57
57
 
58
- # Export all registered components
59
- __all__ = list(DJANGO_CFG_REGISTRY.keys())
58
+ # Export all registered components + feature detection
59
+ __all__ = list(DJANGO_CFG_REGISTRY.keys()) + [
60
+ "is_feature_available",
61
+ "require_feature",
62
+ "register_feature",
63
+ ]
@@ -96,7 +96,7 @@ class CentrifugoLogAdmin(PydanticAdmin):
96
96
 
97
97
  try:
98
98
  formatted = json.dumps(obj.data, indent=2)
99
- return f'<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; max-height: 400px; overflow: auto; font-size: 12px; line-height: 1.5;">{formatted}</pre>'
99
+ return self.html.code_block(formatted, language="json", max_height="400px")
100
100
  except Exception:
101
101
  return str(obj.data)
102
102
 
@@ -106,37 +106,22 @@ class CentrifugoLogAdmin(PydanticAdmin):
106
106
  """Display error information if publish failed."""
107
107
  if obj.is_successful or obj.status == "pending":
108
108
  return self.html.inline(
109
- [
110
- self.html.icon(Icons.CHECK_CIRCLE, size="sm"),
111
- self.html.span("No errors", "text-green-600"),
112
- ]
109
+ self.html.icon(Icons.CHECK_CIRCLE, size="sm"),
110
+ self.html.text("No errors", variant="success"),
111
+ separator=" "
113
112
  )
114
113
 
115
- details = []
116
-
117
- if obj.error_code:
118
- details.append(
119
- self.html.inline(
120
- [
121
- self.html.span("Error Code:", "font-semibold"),
122
- self.html.badge(obj.error_code, variant="danger", icon=Icons.ERROR),
123
- ],
124
- separator=" ",
125
- )
126
- )
114
+ error_code_line = self.html.key_value(
115
+ "Error Code",
116
+ self.html.badge(obj.error_code, variant="danger", icon=Icons.ERROR)
117
+ ) if obj.error_code else None
127
118
 
128
- if obj.error_message:
129
- details.append(
130
- self.html.inline(
131
- [
132
- self.html.span("Message:", "font-semibold"),
133
- self.html.span(obj.error_message, "text-red-600"),
134
- ],
135
- separator=" ",
136
- )
137
- )
119
+ error_msg_line = self.html.key_value(
120
+ "Message",
121
+ self.html.text(obj.error_message, variant="danger")
122
+ ) if obj.error_message else None
138
123
 
139
- return "<br>".join(details) if details else self.html.empty()
124
+ return self.html.breakdown(error_code_line, error_msg_line) if (error_code_line or error_msg_line) else self.html.empty()
140
125
 
141
126
  error_details_display.short_description = "Error Details"
142
127
 
@@ -145,57 +130,34 @@ class CentrifugoLogAdmin(PydanticAdmin):
145
130
  if not obj.wait_for_ack:
146
131
  return self.html.empty("No ACK tracking")
147
132
 
148
- stats = []
149
-
150
133
  # ACK timeout
151
- if obj.ack_timeout:
152
- stats.append(
153
- self.html.inline(
154
- [
155
- self.html.span("Timeout:", "font-semibold"),
156
- self.html.span(f"{obj.ack_timeout}s", "text-gray-600"),
157
- ],
158
- separator=" ",
159
- )
160
- )
134
+ timeout_line = self.html.key_value(
135
+ "Timeout",
136
+ f"{obj.ack_timeout}s"
137
+ ) if obj.ack_timeout else None
161
138
 
162
139
  # ACKs received
163
- stats.append(
164
- self.html.inline(
165
- [
166
- self.html.span("ACKs Received:", "font-semibold"),
167
- self.html.badge(str(obj.acks_received), variant="info"),
168
- ],
169
- separator=" ",
170
- )
140
+ received_line = self.html.key_value(
141
+ "ACKs Received",
142
+ self.html.badge(str(obj.acks_received), variant="info")
171
143
  )
172
144
 
173
145
  # ACKs expected (if known)
174
- if obj.acks_expected:
175
- stats.append(
176
- self.html.inline(
177
- [
178
- self.html.span("ACKs Expected:", "font-semibold"),
179
- self.html.span(str(obj.acks_expected), "text-gray-600"),
180
- ],
181
- separator=" ",
182
- )
146
+ expected_line = self.html.key_value(
147
+ "ACKs Expected",
148
+ str(obj.acks_expected)
149
+ ) if obj.acks_expected else None
150
+
151
+ # Delivery rate
152
+ rate_line = None
153
+ if obj.acks_expected and obj.delivery_rate is not None:
154
+ rate_pct = obj.delivery_rate * 100
155
+ rate_line = self.html.key_value(
156
+ "Delivery Rate",
157
+ self.html.number(rate_pct, precision=1, suffix="%")
183
158
  )
184
159
 
185
- # Delivery rate
186
- if obj.delivery_rate is not None:
187
- rate_pct = obj.delivery_rate * 100
188
- stats.append(
189
- self.html.inline(
190
- [
191
- self.html.span("Delivery Rate:", "font-semibold"),
192
- self.html.span(f"{rate_pct:.1f}%", "text-blue-600"),
193
- ],
194
- separator=" ",
195
- )
196
- )
197
-
198
- return "<br>".join(stats) if stats else self.html.empty()
160
+ return self.html.breakdown(timeout_line, received_line, expected_line, rate_line)
199
161
 
200
162
  delivery_stats_display.short_description = "Delivery Statistics"
201
163
 
@@ -0,0 +1,9 @@
1
+ """
2
+ django_cfg.apps.grpc
3
+
4
+ gRPC integration for django-cfg with Django ORM, JWT auth, monitoring, and admin interface.
5
+ """
6
+
7
+ default_app_config = 'django_cfg.apps.grpc.apps.GRPCAppConfig'
8
+
9
+ __version__ = '1.0.0'
@@ -0,0 +1,11 @@
1
+ """
2
+ Admin interface for gRPC app.
3
+ """
4
+
5
+ from .config import grpcrequestlog_config
6
+ from .grpc_request_log import GRPCRequestLogAdmin
7
+
8
+ __all__ = [
9
+ "GRPCRequestLogAdmin",
10
+ "grpcrequestlog_config",
11
+ ]
@@ -0,0 +1,89 @@
1
+ """
2
+ Admin configuration for gRPC models.
3
+
4
+ Declarative AdminConfig using PydanticAdmin patterns.
5
+ """
6
+
7
+ from django_cfg.modules.django_admin import (
8
+ AdminConfig,
9
+ BadgeField,
10
+ DateTimeField,
11
+ Icons,
12
+ UserField,
13
+ )
14
+
15
+ from ..models import GRPCRequestLog
16
+
17
+
18
+ # Declarative configuration for GRPCRequestLog
19
+ grpcrequestlog_config = AdminConfig(
20
+ model=GRPCRequestLog,
21
+ # Performance optimization
22
+ select_related=["user"],
23
+
24
+ # List display
25
+ list_display=[
26
+ "full_method",
27
+ "service_badge",
28
+ "method_badge",
29
+ "status",
30
+ "grpc_status_code_display",
31
+ "user",
32
+ "duration_display",
33
+ "created_at",
34
+ "completed_at"
35
+ ],
36
+
37
+ # Auto-generated display methods
38
+ display_fields=[
39
+ BadgeField(name="service_name", title="Service", variant="info", icon=Icons.API),
40
+ BadgeField(name="method_name", title="Method", variant="secondary", icon=Icons.CODE),
41
+ BadgeField(
42
+ name="status",
43
+ title="Status",
44
+ label_map={
45
+ "pending": "warning",
46
+ "success": "success",
47
+ "error": "danger",
48
+ "cancelled": "secondary",
49
+ "timeout": "danger",
50
+ },
51
+ ),
52
+ UserField(name="user", title="User", header=True),
53
+ DateTimeField(name="created_at", title="Created", ordering="created_at"),
54
+ DateTimeField(name="completed_at", title="Completed", ordering="completed_at"),
55
+ ],
56
+ # Filters
57
+ list_filter=["status", "grpc_status_code", "service_name", "method_name", "is_authenticated", "created_at"],
58
+ search_fields=[
59
+ "request_id",
60
+ "service_name",
61
+ "method_name",
62
+ "full_method",
63
+ "user__username",
64
+ "user__email",
65
+ "error_message",
66
+ "client_ip",
67
+ ],
68
+ # Autocomplete for user field
69
+ autocomplete_fields=["user"],
70
+ # Readonly fields
71
+ readonly_fields=[
72
+ "id",
73
+ "request_id",
74
+ "created_at",
75
+ "completed_at",
76
+ "request_data_display",
77
+ "response_data_display",
78
+ "error_details_display",
79
+ "performance_stats_display",
80
+ "client_info_display",
81
+ ],
82
+ # Date hierarchy
83
+ date_hierarchy="created_at",
84
+ # Per page
85
+ list_per_page=50,
86
+ )
87
+
88
+
89
+ __all__ = ["grpcrequestlog_config"]
@@ -0,0 +1,252 @@
1
+ """
2
+ gRPC Request Log Admin.
3
+
4
+ PydanticAdmin for GRPCRequestLog model with custom computed fields.
5
+ """
6
+
7
+ import json
8
+
9
+ from django.contrib import admin
10
+ from django_cfg.modules.django_admin import Icons, computed_field
11
+ from django_cfg.modules.django_admin.base import PydanticAdmin
12
+
13
+ from ..models import GRPCRequestLog
14
+ from .config import grpcrequestlog_config
15
+
16
+
17
+ @admin.register(GRPCRequestLog)
18
+ class GRPCRequestLogAdmin(PydanticAdmin):
19
+ """
20
+ gRPC request log admin with analytics and filtering.
21
+
22
+ Features:
23
+ - Color-coded status badges
24
+ - Performance metrics visualization
25
+ - Duration display with performance indicators
26
+ - Formatted JSON for request/response data
27
+ - Error details with highlighted display
28
+ """
29
+
30
+ config = grpcrequestlog_config
31
+
32
+ @computed_field("Service", ordering="service_name")
33
+ def service_badge(self, obj):
34
+ """Display service name as badge."""
35
+ return self.html.badge(obj.service_name, variant="info", icon=Icons.API)
36
+
37
+ @computed_field("Method", ordering="method_name")
38
+ def method_badge(self, obj):
39
+ """Display method name as badge."""
40
+ return self.html.badge(obj.method_name, variant="secondary", icon=Icons.CODE)
41
+
42
+ @computed_field("gRPC Status", ordering="grpc_status_code")
43
+ def grpc_status_code_display(self, obj):
44
+ """Display gRPC status code with color coding."""
45
+ if not obj.grpc_status_code:
46
+ return self.html.empty()
47
+
48
+ # Color code based on status
49
+ if obj.grpc_status_code == "OK":
50
+ variant = "success"
51
+ icon = Icons.CHECK_CIRCLE
52
+ elif obj.grpc_status_code in ["CANCELLED", "DEADLINE_EXCEEDED"]:
53
+ variant = "warning"
54
+ icon = Icons.TIMER
55
+ else:
56
+ variant = "danger"
57
+ icon = Icons.ERROR
58
+
59
+ return self.html.badge(obj.grpc_status_code, variant=variant, icon=icon)
60
+
61
+ @computed_field("Duration", ordering="duration_ms")
62
+ def duration_display(self, obj):
63
+ """Display duration with color coding based on speed."""
64
+ if obj.duration_ms is None:
65
+ return self.html.empty()
66
+
67
+ # Color code based on duration
68
+ if obj.duration_ms < 100:
69
+ variant = "success" # Fast
70
+ icon = Icons.SPEED
71
+ elif obj.duration_ms < 1000:
72
+ variant = "warning" # Moderate
73
+ icon = Icons.TIMER
74
+ else:
75
+ variant = "danger" # Slow
76
+ icon = Icons.ERROR
77
+
78
+ return self.html.badge(f"{obj.duration_ms}ms", variant=variant, icon=icon)
79
+
80
+ def request_data_display(self, obj):
81
+ """Display formatted JSON request data."""
82
+ if not obj.request_data:
83
+ return self.html.empty("No request data logged")
84
+
85
+ try:
86
+ formatted = json.dumps(obj.request_data, indent=2)
87
+ return self.html.code_block(formatted, language="json", max_height="400px")
88
+ except Exception:
89
+ return str(obj.request_data)
90
+
91
+ request_data_display.short_description = "Request Data"
92
+
93
+ def response_data_display(self, obj):
94
+ """Display formatted JSON response data."""
95
+ if not obj.response_data:
96
+ return self.html.empty("No response data logged")
97
+
98
+ try:
99
+ formatted = json.dumps(obj.response_data, indent=2)
100
+ return self.html.code_block(formatted, language="json", max_height="400px")
101
+ except Exception:
102
+ return str(obj.response_data)
103
+
104
+ response_data_display.short_description = "Response Data"
105
+
106
+ def error_details_display(self, obj):
107
+ """Display error information if request failed."""
108
+ if obj.is_successful or obj.status == "pending":
109
+ return self.html.inline(
110
+ self.html.icon(Icons.CHECK_CIRCLE, size="sm"),
111
+ self.html.text("No errors", variant="success"),
112
+ separator=" "
113
+ )
114
+
115
+ # gRPC status code
116
+ code_line = self.html.key_value(
117
+ "gRPC Status",
118
+ self.html.badge(obj.grpc_status_code, variant="danger", icon=Icons.ERROR)
119
+ ) if obj.grpc_status_code else None
120
+
121
+ # Error message
122
+ msg_line = self.html.key_value(
123
+ "Message",
124
+ self.html.text(obj.error_message, variant="danger")
125
+ ) if obj.error_message else None
126
+
127
+ # Error details
128
+ details_line = None
129
+ if obj.error_details:
130
+ try:
131
+ formatted = json.dumps(obj.error_details, indent=2)
132
+ details_line = self.html.key_value(
133
+ "Details",
134
+ self.html.code_block(formatted, language="json", max_height="200px")
135
+ )
136
+ except Exception:
137
+ pass
138
+
139
+ return self.html.breakdown(code_line, msg_line, details_line) if (code_line or msg_line) else self.html.empty()
140
+
141
+ error_details_display.short_description = "Error Details"
142
+
143
+ def performance_stats_display(self, obj):
144
+ """Display performance statistics."""
145
+ # Duration
146
+ duration_line = self.html.key_value(
147
+ "Duration",
148
+ self.html.number(obj.duration_ms, suffix="ms") if obj.duration_ms else "N/A"
149
+ )
150
+
151
+ # Request size
152
+ request_size_line = self.html.key_value(
153
+ "Request Size",
154
+ self.html.number(obj.request_size, suffix=" bytes") if obj.request_size else "N/A"
155
+ )
156
+
157
+ # Response size
158
+ response_size_line = self.html.key_value(
159
+ "Response Size",
160
+ self.html.number(obj.response_size, suffix=" bytes") if obj.response_size else "N/A"
161
+ )
162
+
163
+ # Authentication
164
+ auth_line = self.html.key_value(
165
+ "Authenticated",
166
+ self.html.badge("Yes" if obj.is_authenticated else "No",
167
+ variant="success" if obj.is_authenticated else "secondary")
168
+ )
169
+
170
+ return self.html.breakdown(duration_line, request_size_line, response_size_line, auth_line)
171
+
172
+ performance_stats_display.short_description = "Performance Statistics"
173
+
174
+ def client_info_display(self, obj):
175
+ """Display client information."""
176
+ # Client IP
177
+ ip_line = self.html.key_value(
178
+ "Client IP",
179
+ obj.client_ip if obj.client_ip else "N/A"
180
+ )
181
+
182
+ # User Agent
183
+ ua_line = self.html.key_value(
184
+ "User Agent",
185
+ obj.user_agent if obj.user_agent else "N/A"
186
+ )
187
+
188
+ # Peer
189
+ peer_line = self.html.key_value(
190
+ "Peer",
191
+ self.html.text(obj.peer, variant="secondary") if obj.peer else "N/A"
192
+ )
193
+
194
+ return self.html.breakdown(ip_line, ua_line, peer_line)
195
+
196
+ client_info_display.short_description = "Client Information"
197
+
198
+ # Fieldsets for detail view
199
+ def get_fieldsets(self, request, obj=None):
200
+ """Dynamic fieldsets based on object state."""
201
+ fieldsets = [
202
+ (
203
+ "Request Information",
204
+ {"fields": ("id", "request_id", "full_method", "service_name", "method_name", "status")},
205
+ ),
206
+ (
207
+ "User Context",
208
+ {"fields": ("user", "is_authenticated")},
209
+ ),
210
+ (
211
+ "Performance",
212
+ {"fields": ("performance_stats_display", "duration_ms", "created_at", "completed_at")},
213
+ ),
214
+ (
215
+ "Client Information",
216
+ {"fields": ("client_info_display", "client_ip", "user_agent", "peer"), "classes": ("collapse",)},
217
+ ),
218
+ ]
219
+
220
+ # Add request/response data sections if available
221
+ if obj and obj.request_data:
222
+ fieldsets.insert(
223
+ 3,
224
+ (
225
+ "Request Data",
226
+ {"fields": ("request_data_display",), "classes": ("collapse",)},
227
+ ),
228
+ )
229
+
230
+ if obj and obj.response_data:
231
+ fieldsets.insert(
232
+ 4,
233
+ (
234
+ "Response Data",
235
+ {"fields": ("response_data_display",), "classes": ("collapse",)},
236
+ ),
237
+ )
238
+
239
+ # Add error section only if failed
240
+ if obj and not obj.is_successful and obj.status != "pending":
241
+ fieldsets.insert(
242
+ 5,
243
+ (
244
+ "Error Details",
245
+ {"fields": ("error_details_display", "grpc_status_code", "error_message", "error_details")},
246
+ ),
247
+ )
248
+
249
+ return fieldsets
250
+
251
+
252
+ __all__ = ["GRPCRequestLogAdmin"]
@@ -0,0 +1,28 @@
1
+ """
2
+ Django app configuration for gRPC integration.
3
+ """
4
+
5
+ from django.apps import AppConfig
6
+
7
+
8
+ class GRPCAppConfig(AppConfig):
9
+ """
10
+ Django app config for gRPC integration.
11
+
12
+ Provides:
13
+ - gRPC server with Django ORM integration
14
+ - JWT authentication
15
+ - Request logging to database
16
+ - Admin interface for monitoring
17
+ - REST API for metrics
18
+ """
19
+
20
+ default_auto_field = 'django.db.models.BigAutoField'
21
+ name = 'django_cfg.apps.grpc'
22
+ verbose_name = 'gRPC Integration'
23
+
24
+ def ready(self):
25
+ """Called when Django starts."""
26
+ # Import signal handlers if needed
27
+ # from . import signals
28
+ pass
@@ -0,0 +1,9 @@
1
+ """
2
+ gRPC authentication components.
3
+
4
+ Provides JWT authentication for gRPC services.
5
+ """
6
+
7
+ from .jwt_auth import JWTAuthInterceptor
8
+
9
+ __all__ = ["JWTAuthInterceptor"]