django-cfg 1.5.8__py3-none-any.whl → 1.5.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (119) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/commands/serializers.py +152 -0
  3. django_cfg/apps/api/commands/views.py +32 -0
  4. django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
  5. django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
  6. django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
  7. django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
  8. django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
  9. django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
  10. django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
  11. django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
  12. django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
  13. django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
  14. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +5 -5
  15. django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
  16. django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
  17. django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
  18. django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
  19. django_cfg/apps/integrations/grpc/admin/config.py +113 -9
  20. django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
  21. django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
  22. django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
  23. django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
  24. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
  25. django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
  26. django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
  27. django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
  28. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +130 -0
  29. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +171 -96
  30. django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
  31. django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
  32. django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
  33. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
  34. django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
  35. django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
  36. django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
  37. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
  38. django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
  39. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
  40. django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
  41. django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
  42. django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
  43. django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
  44. django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
  45. django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
  46. django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
  47. django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
  48. django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
  49. django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
  50. django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
  51. django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
  52. django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
  53. django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
  54. django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
  55. django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
  56. django_cfg/apps/integrations/grpc/urls.py +8 -0
  57. django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
  58. django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
  59. django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
  60. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +177 -0
  61. django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
  62. django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
  63. django_cfg/apps/integrations/grpc/views/charts.py +21 -14
  64. django_cfg/apps/integrations/grpc/views/config.py +8 -6
  65. django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
  66. django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
  67. django_cfg/apps/integrations/grpc/views/services.py +30 -21
  68. django_cfg/apps/integrations/grpc/views/testing.py +45 -43
  69. django_cfg/apps/integrations/rq/views/jobs.py +19 -9
  70. django_cfg/apps/integrations/rq/views/schedule.py +7 -3
  71. django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
  72. django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
  73. django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
  74. django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
  75. django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
  76. django_cfg/config.py +33 -0
  77. django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
  78. django_cfg/management/commands/check_endpoints.py +2 -2
  79. django_cfg/management/commands/check_settings.py +3 -10
  80. django_cfg/management/commands/clear_constance.py +3 -10
  81. django_cfg/management/commands/create_token.py +4 -11
  82. django_cfg/management/commands/list_urls.py +4 -10
  83. django_cfg/management/commands/migrate_all.py +18 -12
  84. django_cfg/management/commands/migrator.py +4 -11
  85. django_cfg/management/commands/script.py +4 -10
  86. django_cfg/management/commands/show_config.py +8 -16
  87. django_cfg/management/commands/show_urls.py +5 -11
  88. django_cfg/management/commands/superuser.py +4 -11
  89. django_cfg/management/commands/tree.py +5 -10
  90. django_cfg/management/utils/README.md +402 -0
  91. django_cfg/management/utils/__init__.py +29 -0
  92. django_cfg/management/utils/mixins.py +176 -0
  93. django_cfg/middleware/pagination.py +53 -54
  94. django_cfg/models/api/grpc/__init__.py +15 -21
  95. django_cfg/models/api/grpc/config.py +155 -73
  96. django_cfg/models/ngrok/config.py +7 -6
  97. django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
  98. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
  99. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
  100. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
  101. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
  102. django_cfg/modules/django_client/core/parser/base.py +114 -30
  103. django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
  104. django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
  105. django_cfg/modules/django_email/management/commands/test_email.py +4 -10
  106. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
  107. django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
  108. django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
  109. django_cfg/modules/django_unfold/navigation.py +6 -18
  110. django_cfg/pyproject.toml +1 -1
  111. django_cfg/registry/modules.py +1 -4
  112. django_cfg/requirements.txt +52 -0
  113. django_cfg/static/frontend/admin.zip +0 -0
  114. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/METADATA +1 -1
  115. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/RECORD +118 -97
  116. django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
  117. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/WHEEL +0 -0
  118. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/entry_points.txt +0 -0
  119. {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,214 @@
1
+ """
2
+ gRPC Proto Files ViewSet.
3
+
4
+ Provides REST API endpoints for downloading proto files generated from Django models.
5
+ """
6
+
7
+ from django.http import FileResponse, HttpResponse
8
+ from django_cfg.mixins import AdminAPIMixin
9
+ from django_cfg.modules.django_logging import get_logger
10
+ from drf_spectacular.types import OpenApiTypes
11
+ from drf_spectacular.utils import OpenApiParameter, extend_schema
12
+ from rest_framework import status, viewsets
13
+ from rest_framework.decorators import action
14
+ from rest_framework.response import Response
15
+
16
+ from ..serializers.proto_files import (
17
+ ProtoFileListSerializer,
18
+ ProtoGenerateRequestSerializer,
19
+ ProtoGenerateResponseSerializer,
20
+ )
21
+ from ..services import ProtoFilesManager
22
+
23
+ logger = get_logger("grpc.proto_files")
24
+
25
+
26
+ class GRPCProtoFilesViewSet(AdminAPIMixin, viewsets.ViewSet):
27
+ """
28
+ ViewSet for gRPC proto files management.
29
+
30
+ Provides endpoints for:
31
+ - List all available proto files
32
+ - Download specific proto file
33
+ - Download all proto files as .zip
34
+ - Trigger proto generation
35
+
36
+ Requires admin authentication (JWT, Session, or Basic Auth).
37
+ """
38
+
39
+ lookup_value_regex = r'[^/]+'
40
+
41
+ def __init__(self, **kwargs):
42
+ super().__init__(**kwargs)
43
+ self.manager = ProtoFilesManager()
44
+
45
+ @extend_schema(
46
+ tags=["gRPC Proto Files"],
47
+ summary="List all proto files",
48
+ description="Returns list of all available proto files with metadata.",
49
+ responses={
50
+ 200: ProtoFileListSerializer,
51
+ },
52
+ )
53
+ def list(self, request):
54
+ """List all available proto files."""
55
+ try:
56
+ from django.urls import reverse
57
+ from django_cfg.core.state import get_current_config
58
+
59
+ proto_files = self.manager.scan_proto_files(request=request)
60
+
61
+ config = get_current_config()
62
+
63
+ # Build download-all URL
64
+ # Use api_url from config (respects HTTPS behind reverse proxy)
65
+ # Falls back to request.build_absolute_uri if config not available
66
+ if config and hasattr(config, 'api_url'):
67
+ path = reverse('django_cfg_grpc:proto-files-download-all')
68
+ download_all_url = f"{config.api_url}{path}"
69
+ else:
70
+ download_all_url = request.build_absolute_uri(
71
+ reverse('django_cfg_grpc:proto-files-download-all')
72
+ )
73
+
74
+ response_data = {
75
+ "files": proto_files,
76
+ "total_files": len(proto_files),
77
+ "proto_dir": str(self.manager.get_proto_dir()),
78
+ "download_all_url": download_all_url,
79
+ }
80
+
81
+ serializer = ProtoFileListSerializer(data=response_data)
82
+ serializer.is_valid(raise_exception=True)
83
+ return Response(serializer.data)
84
+
85
+ except Exception as e:
86
+ logger.error(f"Proto files list error: {e}", exc_info=True)
87
+ return Response(
88
+ {"error": "Internal server error"},
89
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
90
+ )
91
+
92
+ @extend_schema(
93
+ tags=["gRPC Proto Files"],
94
+ summary="Download proto file",
95
+ description="Download specific proto file by app label.",
96
+ parameters=[
97
+ OpenApiParameter(
98
+ name="pk",
99
+ type=OpenApiTypes.STR,
100
+ location=OpenApiParameter.PATH,
101
+ description="App label (e.g., 'crypto')",
102
+ required=True,
103
+ ),
104
+ ],
105
+ responses={
106
+ 200: {
107
+ "description": "Proto file content",
108
+ "content": {"text/plain": {}},
109
+ },
110
+ 404: {"description": "Proto file not found"},
111
+ },
112
+ )
113
+ def retrieve(self, request, pk=None):
114
+ """Download specific proto file."""
115
+ try:
116
+ app_label = pk
117
+ proto_file = self.manager.get_proto_file(app_label)
118
+
119
+ if not proto_file:
120
+ return Response(
121
+ {"error": f"Proto file for app '{app_label}' not found"},
122
+ status=status.HTTP_404_NOT_FOUND,
123
+ )
124
+
125
+ # Return proto file content
126
+ response = FileResponse(
127
+ open(proto_file, "rb"),
128
+ content_type="text/plain; charset=utf-8",
129
+ )
130
+ response["Content-Disposition"] = f'attachment; filename="{proto_file.name}"'
131
+ return response
132
+
133
+ except Exception as e:
134
+ logger.error(f"Proto file download error: {e}", exc_info=True)
135
+ return Response(
136
+ {"error": "Internal server error"},
137
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
138
+ )
139
+
140
+ @extend_schema(
141
+ tags=["gRPC Proto Files"],
142
+ summary="Download all proto files",
143
+ description="Download all proto files as a .zip archive.",
144
+ responses={
145
+ 200: {
146
+ "description": "Zip archive with all proto files",
147
+ "content": {"application/zip": {}},
148
+ },
149
+ 404: {"description": "No proto files found"},
150
+ },
151
+ )
152
+ @action(detail=False, methods=["get"], url_path="download-all")
153
+ def download_all(self, request):
154
+ """Download all proto files as .zip archive."""
155
+ try:
156
+ zip_data = self.manager.create_zip_archive()
157
+
158
+ if not zip_data:
159
+ return Response(
160
+ {"error": "No proto files found"},
161
+ status=status.HTTP_404_NOT_FOUND,
162
+ )
163
+
164
+ # Return zip file
165
+ response = HttpResponse(zip_data, content_type="application/zip")
166
+ response["Content-Disposition"] = 'attachment; filename="protos.zip"'
167
+ return response
168
+
169
+ except Exception as e:
170
+ logger.error(f"Proto files zip error: {e}", exc_info=True)
171
+ return Response(
172
+ {"error": "Internal server error"},
173
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
174
+ )
175
+
176
+ @extend_schema(
177
+ tags=["gRPC Proto Files"],
178
+ summary="Generate proto files",
179
+ description="Trigger proto file generation for specified apps.",
180
+ request=ProtoGenerateRequestSerializer,
181
+ responses={
182
+ 200: ProtoGenerateResponseSerializer,
183
+ 400: {"description": "Bad request"},
184
+ },
185
+ )
186
+ @action(detail=False, methods=["post"], url_path="generate")
187
+ def generate(self, request):
188
+ """Trigger proto generation for specified apps."""
189
+ try:
190
+ serializer = ProtoGenerateRequestSerializer(data=request.data)
191
+ serializer.is_valid(raise_exception=True)
192
+
193
+ apps = serializer.validated_data.get("apps")
194
+ force = serializer.validated_data.get("force", False)
195
+
196
+ # Generate protos via service
197
+ result = self.manager.generate_protos(apps=apps, force=force)
198
+
199
+ # Add proto_dir to response
200
+ result["proto_dir"] = str(self.manager.get_proto_dir())
201
+
202
+ response_serializer = ProtoGenerateResponseSerializer(data=result)
203
+ response_serializer.is_valid(raise_exception=True)
204
+ return Response(response_serializer.data)
205
+
206
+ except Exception as e:
207
+ logger.error(f"Proto generation error: {e}", exc_info=True)
208
+ return Response(
209
+ {"error": "Internal server error"},
210
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
211
+ )
212
+
213
+
214
+ __all__ = ["GRPCProtoFilesViewSet"]
@@ -6,6 +6,7 @@ Provides REST API endpoints for viewing registered gRPC services and their metho
6
6
 
7
7
  from django.db import models
8
8
  from django.db.models import Avg, Count, Max, Min
9
+ from django_cfg.middleware.pagination import DefaultPagination
9
10
  from django_cfg.mixins import AdminAPIMixin
10
11
  from django_cfg.modules.django_logging import get_logger
11
12
  from drf_spectacular.types import OpenApiTypes
@@ -20,13 +21,14 @@ from ..serializers.service_registry import (
20
21
  ServiceDetailSerializer,
21
22
  ServiceListSerializer,
22
23
  ServiceMethodsSerializer,
24
+ ServiceSummarySerializer,
23
25
  )
24
26
  from ..services import ServiceRegistryManager
25
27
 
26
28
  logger = get_logger("grpc.services")
27
29
 
28
30
 
29
- class GRPCServiceViewSet(AdminAPIMixin, viewsets.ViewSet):
31
+ class GRPCServiceViewSet(AdminAPIMixin, viewsets.GenericViewSet):
30
32
  """
31
33
  ViewSet for gRPC service registry and management.
32
34
 
@@ -39,13 +41,20 @@ class GRPCServiceViewSet(AdminAPIMixin, viewsets.ViewSet):
39
41
  Requires admin authentication (JWT, Session, or Basic Auth).
40
42
  """
41
43
 
44
+ # Pagination for list endpoint
45
+ pagination_class = DefaultPagination
46
+
47
+ # Required for GenericViewSet
48
+ queryset = GRPCRequestLog.objects.none() # Placeholder
49
+ serializer_class = ServiceSummarySerializer
50
+
42
51
  # Allow dots in service names (e.g., apps.CryptoService)
43
52
  lookup_value_regex = r'[^/]+'
44
53
 
45
54
  @extend_schema(
46
55
  tags=["gRPC Services"],
47
56
  summary="List all services",
48
- description="Returns list of all registered gRPC services with basic statistics.",
57
+ description="Returns paginated list of all registered gRPC services with basic statistics.",
49
58
  parameters=[
50
59
  OpenApiParameter(
51
60
  name="hours",
@@ -54,13 +63,14 @@ class GRPCServiceViewSet(AdminAPIMixin, viewsets.ViewSet):
54
63
  description="Statistics period in hours (default: 24)",
55
64
  required=False,
56
65
  ),
66
+ # Note: page, page_size added automatically by pagination_class
57
67
  ],
58
68
  responses={
59
- 200: ServiceListSerializer,
69
+ 200: ServiceSummarySerializer(many=True),
60
70
  },
61
71
  )
62
72
  def list(self, request):
63
- """List all registered gRPC services."""
73
+ """List all registered gRPC services with pagination."""
64
74
  try:
65
75
  hours = int(request.GET.get("hours", 24))
66
76
  hours = min(max(hours, 1), 168)
@@ -68,23 +78,20 @@ class GRPCServiceViewSet(AdminAPIMixin, viewsets.ViewSet):
68
78
  # Use service registry manager
69
79
  registry = ServiceRegistryManager()
70
80
 
71
- if not registry.is_server_running():
72
- # No running server - return empty list
73
- return Response({
74
- "services": [],
75
- "total_services": 0,
76
- })
77
-
78
81
  # Get services with stats from service layer
82
+ # Note: This will return empty list if server is not running,
83
+ # but that's expected - services are only known when server is started
79
84
  services_list = registry.get_all_services_with_stats(hours=hours)
80
85
 
81
- response_data = {
82
- "services": services_list,
83
- "total_services": len(services_list),
84
- }
86
+ # Paginate the services list (works with empty list too)
87
+ page = self.paginate_queryset(services_list)
88
+ if page is not None:
89
+ serializer = ServiceSummarySerializer(page, many=True)
90
+ return self.get_paginated_response(serializer.data)
85
91
 
86
- serializer = ServiceListSerializer(**response_data)
87
- return Response(serializer.model_dump())
92
+ # Fallback (no pagination)
93
+ serializer = ServiceSummarySerializer(services_list, many=True)
94
+ return Response(serializer.data)
88
95
 
89
96
  except Exception as e:
90
97
  logger.error(f"Service list error: {e}", exc_info=True)
@@ -216,8 +223,9 @@ class GRPCServiceViewSet(AdminAPIMixin, viewsets.ViewSet):
216
223
  ],
217
224
  }
218
225
 
219
- serializer = ServiceDetailSerializer(**service_detail)
220
- return Response(serializer.model_dump())
226
+ serializer = ServiceDetailSerializer(data=service_detail)
227
+ serializer.is_valid(raise_exception=True)
228
+ return Response(serializer.data)
221
229
 
222
230
  except Exception as e:
223
231
  logger.error(f"Service detail error: {e}", exc_info=True)
@@ -275,8 +283,9 @@ class GRPCServiceViewSet(AdminAPIMixin, viewsets.ViewSet):
275
283
  "total_methods": len(methods_list),
276
284
  }
277
285
 
278
- serializer = ServiceMethodsSerializer(**response_data)
279
- return Response(serializer.model_dump())
286
+ serializer = ServiceMethodsSerializer(data=response_data)
287
+ serializer.is_valid(raise_exception=True)
288
+ return Response(serializer.data)
280
289
 
281
290
  except Exception as e:
282
291
  logger.error(f"Service methods error: {e}", exc_info=True)
@@ -85,8 +85,9 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
85
85
  "total_examples": len(examples),
86
86
  }
87
87
 
88
- serializer = GRPCExamplesListSerializer(**response_data)
89
- return Response(serializer.model_dump())
88
+ serializer = GRPCExamplesListSerializer(data=response_data)
89
+ serializer.is_valid(raise_exception=True)
90
+ return Response(serializer.data)
90
91
 
91
92
  except Exception as e:
92
93
  logger.error(f"Examples fetch error: {e}", exc_info=True)
@@ -147,35 +148,33 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
147
148
  # Serialize paginated data
148
149
  logs_list = []
149
150
  for log in page:
150
- log_serializer = GRPCTestLogSerializer(
151
- request_id=log.request_id,
152
- service=log.service_name,
153
- method=log.method_name,
154
- status=log.status,
155
- grpc_status_code=log.grpc_status_code or "",
156
- error_message=log.error_message or "",
157
- duration_ms=log.duration_ms or 0,
158
- created_at=log.created_at.isoformat(),
159
- user=log.user.username if log.user else None,
160
- )
161
- logs_list.append(log_serializer.model_dump())
151
+ logs_list.append({
152
+ "request_id": log.request_id,
153
+ "service": log.service_name,
154
+ "method": log.method_name,
155
+ "status": log.status,
156
+ "grpc_status_code": log.grpc_status_code or "",
157
+ "error_message": log.error_message or "",
158
+ "duration_ms": log.duration_ms or 0,
159
+ "created_at": log.created_at.isoformat(),
160
+ "user": log.user.username if log.user else None,
161
+ })
162
162
  return self.get_paginated_response(logs_list)
163
163
 
164
164
  # No pagination (shouldn't happen with default pagination)
165
165
  logs_list = []
166
166
  for log in queryset[:100]: # Safety limit
167
- log_serializer = GRPCTestLogSerializer(
168
- request_id=log.request_id,
169
- service=log.service_name,
170
- method=log.method_name,
171
- status=log.status,
172
- grpc_status_code=log.grpc_status_code or "",
173
- error_message=log.error_message or "",
174
- duration_ms=log.duration_ms or 0,
175
- created_at=log.created_at.isoformat(),
176
- user=log.user.username if log.user else None,
177
- )
178
- logs_list.append(log_serializer.model_dump())
167
+ logs_list.append({
168
+ "request_id": log.request_id,
169
+ "service": log.service_name,
170
+ "method": log.method_name,
171
+ "status": log.status,
172
+ "grpc_status_code": log.grpc_status_code or "",
173
+ "error_message": log.error_message or "",
174
+ "duration_ms": log.duration_ms or 0,
175
+ "created_at": log.created_at.isoformat(),
176
+ "user": log.user.username if log.user else None,
177
+ })
179
178
  return Response({"logs": logs_list, "count": len(logs_list)})
180
179
 
181
180
  except Exception as e:
@@ -221,9 +220,10 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
221
220
  from django.utils import timezone
222
221
 
223
222
  try:
224
- # Validate request - Pydantic validation
225
- serializer = GRPCCallRequestSerializer(**request.data)
226
- data = serializer.model_dump()
223
+ # Validate request - DRF validation
224
+ serializer = GRPCCallRequestSerializer(data=request.data)
225
+ serializer.is_valid(raise_exception=True)
226
+ data = serializer.validated_data
227
227
 
228
228
  service_name = data["service"]
229
229
  method_name = data["method"]
@@ -275,21 +275,23 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
275
275
  if response_data and isinstance(response_data, dict):
276
276
  response_data = json.dumps(response_data)
277
277
 
278
- response_serializer = GRPCCallResponseSerializer(
279
- success=result["success"],
280
- request_id=request_id,
281
- service=result["service"],
282
- method=result["method"],
283
- status=log.status,
284
- grpc_status_code=result["grpc_status_code"],
285
- duration_ms=result["duration_ms"],
286
- response=response_data,
287
- error=result.get("error_message"),
288
- metadata={},
289
- timestamp=end_time.isoformat(),
290
- )
278
+ response_data_dict = {
279
+ "success": result["success"],
280
+ "request_id": request_id,
281
+ "service": result["service"],
282
+ "method": result["method"],
283
+ "status": log.status,
284
+ "grpc_status_code": result["grpc_status_code"],
285
+ "duration_ms": result["duration_ms"],
286
+ "response": response_data,
287
+ "error": result.get("error_message"),
288
+ "metadata": {},
289
+ "timestamp": end_time.isoformat(),
290
+ }
291
291
 
292
- return Response(response_serializer.model_dump(), status=status.HTTP_200_OK)
292
+ response_serializer = GRPCCallResponseSerializer(data=response_data_dict)
293
+ response_serializer.is_valid(raise_exception=True)
294
+ return Response(response_serializer.data, status=status.HTTP_200_OK)
293
295
 
294
296
  except Exception as e:
295
297
  logger.error(f"gRPC call endpoint error: {e}", exc_info=True)
@@ -19,7 +19,7 @@ from ..services import job_to_model
19
19
  logger = get_logger("rq.jobs")
20
20
 
21
21
 
22
- class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
22
+ class JobViewSet(AdminAPIMixin, viewsets.GenericViewSet):
23
23
  """
24
24
  ViewSet for RQ job management.
25
25
 
@@ -33,6 +33,8 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
33
33
  Requires admin authentication (JWT, Session, or Basic Auth).
34
34
  """
35
35
 
36
+ serializer_class = JobListSerializer
37
+
36
38
  @extend_schema(
37
39
  tags=["RQ Jobs"],
38
40
  summary="List all jobs",
@@ -455,9 +457,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
455
457
  except Exception as e:
456
458
  logger.debug(f"Failed to get failed jobs for queue {queue_name}: {e}")
457
459
 
458
- serializer = JobListSerializer(data=all_jobs, many=True)
460
+ # Use DRF pagination
461
+ page = self.paginate_queryset(all_jobs)
462
+ serializer = JobListSerializer(data=page, many=True)
459
463
  serializer.is_valid(raise_exception=True)
460
- return Response(serializer.data)
464
+ return self.get_paginated_response(serializer.data)
461
465
 
462
466
  except Exception as e:
463
467
  logger.error(f"Failed jobs list error: {e}", exc_info=True)
@@ -530,9 +534,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
530
534
  except Exception as e:
531
535
  logger.debug(f"Failed to get finished jobs for queue {queue_name}: {e}")
532
536
 
533
- serializer = JobListSerializer(data=all_jobs, many=True)
537
+ # Use DRF pagination
538
+ page = self.paginate_queryset(all_jobs)
539
+ serializer = JobListSerializer(data=page, many=True)
534
540
  serializer.is_valid(raise_exception=True)
535
- return Response(serializer.data)
541
+ return self.get_paginated_response(serializer.data)
536
542
 
537
543
  except Exception as e:
538
544
  logger.error(f"Finished jobs list error: {e}", exc_info=True)
@@ -795,9 +801,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
795
801
  except Exception as e:
796
802
  logger.debug(f"Failed to get deferred jobs for queue {queue_name}: {e}")
797
803
 
798
- serializer = JobListSerializer(data=all_jobs, many=True)
804
+ # Use DRF pagination
805
+ page = self.paginate_queryset(all_jobs)
806
+ serializer = JobListSerializer(data=page, many=True)
799
807
  serializer.is_valid(raise_exception=True)
800
- return Response(serializer.data)
808
+ return self.get_paginated_response(serializer.data)
801
809
 
802
810
  except Exception as e:
803
811
  logger.error(f"Deferred jobs list error: {e}", exc_info=True)
@@ -870,9 +878,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
870
878
  except Exception as e:
871
879
  logger.debug(f"Failed to get started jobs for queue {queue_name}: {e}")
872
880
 
873
- serializer = JobListSerializer(data=all_jobs, many=True)
881
+ # Use DRF pagination
882
+ page = self.paginate_queryset(all_jobs)
883
+ serializer = JobListSerializer(data=page, many=True)
874
884
  serializer.is_valid(raise_exception=True)
875
- return Response(serializer.data)
885
+ return self.get_paginated_response(serializer.data)
876
886
 
877
887
  except Exception as e:
878
888
  logger.error(f"Started jobs list error: {e}", exc_info=True)
@@ -23,7 +23,7 @@ from ..serializers import (
23
23
  logger = get_logger("rq.schedule")
24
24
 
25
25
 
26
- class ScheduleViewSet(AdminAPIMixin, viewsets.ViewSet):
26
+ class ScheduleViewSet(AdminAPIMixin, viewsets.GenericViewSet):
27
27
  """
28
28
  ViewSet for RQ schedule management.
29
29
 
@@ -37,6 +37,8 @@ class ScheduleViewSet(AdminAPIMixin, viewsets.ViewSet):
37
37
  Requires rq-scheduler to be installed: pip install rq-scheduler
38
38
  """
39
39
 
40
+ serializer_class = ScheduledJobSerializer
41
+
40
42
  @extend_schema(
41
43
  tags=["RQ Schedules"],
42
44
  summary="List scheduled jobs",
@@ -131,9 +133,11 @@ class ScheduleViewSet(AdminAPIMixin, viewsets.ViewSet):
131
133
  }
132
134
  all_jobs.append(job_data)
133
135
 
134
- serializer = ScheduledJobSerializer(data=all_jobs, many=True)
136
+ # Use DRF pagination
137
+ page = self.paginate_queryset(all_jobs)
138
+ serializer = ScheduledJobSerializer(data=page, many=True)
135
139
  serializer.is_valid(raise_exception=True)
136
- return Response(serializer.data)
140
+ return self.get_paginated_response(serializer.data)
137
141
 
138
142
  except Exception as e:
139
143
  logger.error(f"Failed to list scheduled jobs: {e}", exc_info=True)
@@ -8,7 +8,14 @@ from rest_framework import serializers
8
8
 
9
9
 
10
10
  class CommandSerializer(serializers.Serializer):
11
- """Django management command serializer."""
11
+ """
12
+ Django management command serializer.
13
+
14
+ Includes security metadata from base classes (SafeCommand, InteractiveCommand, etc.):
15
+ - web_executable: Can be executed via web interface
16
+ - requires_input: Requires interactive user input
17
+ - is_destructive: Modifies or deletes data
18
+ """
12
19
  name = serializers.CharField()
13
20
  app = serializers.CharField()
14
21
  help = serializers.CharField()
@@ -17,6 +24,23 @@ class CommandSerializer(serializers.Serializer):
17
24
  is_allowed = serializers.BooleanField(required=False)
18
25
  risk_level = serializers.CharField(required=False)
19
26
 
27
+ # Security metadata from command base classes
28
+ web_executable = serializers.BooleanField(
29
+ required=False,
30
+ allow_null=True,
31
+ help_text="Can be executed via web interface"
32
+ )
33
+ requires_input = serializers.BooleanField(
34
+ required=False,
35
+ allow_null=True,
36
+ help_text="Requires interactive user input"
37
+ )
38
+ is_destructive = serializers.BooleanField(
39
+ required=False,
40
+ allow_null=True,
41
+ help_text="Modifies or deletes data"
42
+ )
43
+
20
44
 
21
45
  class CommandsSummarySerializer(serializers.Serializer):
22
46
  """Commands summary serializer."""
@@ -55,7 +55,7 @@ class CommandsService:
55
55
  continue
56
56
 
57
57
  try:
58
- # Try to load command to get help text
58
+ # Try to load command to get help text and metadata
59
59
  command = load_command_class(app_name, command_name)
60
60
  help_text = getattr(command, 'help', 'No description available')
61
61
 
@@ -66,6 +66,11 @@ class CommandsService:
66
66
  # Get risk level
67
67
  risk_level = get_command_risk_level(command_name, app_name)
68
68
 
69
+ # Extract security metadata from command instance
70
+ web_executable = getattr(command, 'web_executable', None)
71
+ requires_input = getattr(command, 'requires_input', None)
72
+ is_destructive = getattr(command, 'is_destructive', None)
73
+
69
74
  commands_list.append({
70
75
  'name': command_name,
71
76
  'app': app_name,
@@ -74,6 +79,9 @@ class CommandsService:
74
79
  'is_custom': is_custom,
75
80
  'is_allowed': is_allowed,
76
81
  'risk_level': risk_level,
82
+ 'web_executable': web_executable,
83
+ 'requires_input': requires_input,
84
+ 'is_destructive': is_destructive,
77
85
  })
78
86
  except Exception as e:
79
87
  # If we can't load the command, still include basic info
@@ -86,6 +94,9 @@ class CommandsService:
86
94
  'is_custom': app_name == 'django_cfg',
87
95
  'is_allowed': is_allowed,
88
96
  'risk_level': get_command_risk_level(command_name, app_name),
97
+ 'web_executable': None,
98
+ 'requires_input': None,
99
+ 'is_destructive': None,
89
100
  })
90
101
 
91
102
  # Sort by name
@@ -7,16 +7,19 @@ Usage: python manage.py maintenance enable/disable/status/sync domain.com
7
7
 
8
8
  from typing import Any
9
9
 
10
- from django.core.management.base import BaseCommand, CommandError
10
+ from django.core.management.base import CommandError
11
11
  from django.db import transaction
12
12
 
13
+ from django_cfg.management.utils import InteractiveCommand
14
+
13
15
  from ...models import CloudflareSite, MaintenanceLog
14
16
  from ...services import MaintenanceService
15
17
 
16
18
 
17
- class Command(BaseCommand):
19
+ class Command(InteractiveCommand):
18
20
  """Simple maintenance management command."""
19
21
 
22
+ command_name = 'maintenance'
20
23
  help = 'Manage maintenance mode for Cloudflare sites'
21
24
 
22
25
  def add_arguments(self, parser) -> None: