django-cfg 1.5.8__py3-none-any.whl → 1.5.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/commands/serializers.py +152 -0
- django_cfg/apps/api/commands/views.py +32 -0
- django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
- django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
- django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
- django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
- django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
- django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
- django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
- django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +5 -5
- django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
- django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
- django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
- django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
- django_cfg/apps/integrations/grpc/admin/config.py +113 -9
- django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
- django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
- django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
- django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
- django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
- django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
- django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +130 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +171 -96
- django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
- django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
- django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
- django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
- django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
- django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
- django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
- django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
- django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
- django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
- django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
- django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
- django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
- django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
- django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
- django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
- django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
- django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
- django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
- django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
- django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
- django_cfg/apps/integrations/grpc/urls.py +8 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
- django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +177 -0
- django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
- django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
- django_cfg/apps/integrations/grpc/views/charts.py +21 -14
- django_cfg/apps/integrations/grpc/views/config.py +8 -6
- django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
- django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
- django_cfg/apps/integrations/grpc/views/services.py +30 -21
- django_cfg/apps/integrations/grpc/views/testing.py +45 -43
- django_cfg/apps/integrations/rq/views/jobs.py +19 -9
- django_cfg/apps/integrations/rq/views/schedule.py +7 -3
- django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
- django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
- django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
- django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
- django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
- django_cfg/config.py +33 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
- django_cfg/management/commands/check_endpoints.py +2 -2
- django_cfg/management/commands/check_settings.py +3 -10
- django_cfg/management/commands/clear_constance.py +3 -10
- django_cfg/management/commands/create_token.py +4 -11
- django_cfg/management/commands/list_urls.py +4 -10
- django_cfg/management/commands/migrate_all.py +18 -12
- django_cfg/management/commands/migrator.py +4 -11
- django_cfg/management/commands/script.py +4 -10
- django_cfg/management/commands/show_config.py +8 -16
- django_cfg/management/commands/show_urls.py +5 -11
- django_cfg/management/commands/superuser.py +4 -11
- django_cfg/management/commands/tree.py +5 -10
- django_cfg/management/utils/README.md +402 -0
- django_cfg/management/utils/__init__.py +29 -0
- django_cfg/management/utils/mixins.py +176 -0
- django_cfg/middleware/pagination.py +53 -54
- django_cfg/models/api/grpc/__init__.py +15 -21
- django_cfg/models/api/grpc/config.py +155 -73
- django_cfg/models/ngrok/config.py +7 -6
- django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
- django_cfg/modules/django_client/core/parser/base.py +114 -30
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
- django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
- django_cfg/modules/django_email/management/commands/test_email.py +4 -10
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
- django_cfg/modules/django_unfold/navigation.py +6 -18
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/modules.py +1 -4
- django_cfg/requirements.txt +52 -0
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/METADATA +1 -1
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/RECORD +118 -97
- django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.14.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,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.
|
|
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:
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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(
|
|
220
|
-
|
|
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(
|
|
279
|
-
|
|
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(
|
|
89
|
-
|
|
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
|
-
|
|
151
|
-
request_id
|
|
152
|
-
service
|
|
153
|
-
method
|
|
154
|
-
status
|
|
155
|
-
grpc_status_code
|
|
156
|
-
error_message
|
|
157
|
-
duration_ms
|
|
158
|
-
created_at
|
|
159
|
-
user
|
|
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
|
-
|
|
168
|
-
request_id
|
|
169
|
-
service
|
|
170
|
-
method
|
|
171
|
-
status
|
|
172
|
-
grpc_status_code
|
|
173
|
-
error_message
|
|
174
|
-
duration_ms
|
|
175
|
-
created_at
|
|
176
|
-
user
|
|
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 -
|
|
225
|
-
serializer = GRPCCallRequestSerializer(
|
|
226
|
-
|
|
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
|
-
|
|
279
|
-
success
|
|
280
|
-
request_id
|
|
281
|
-
service
|
|
282
|
-
method
|
|
283
|
-
status
|
|
284
|
-
grpc_status_code
|
|
285
|
-
duration_ms
|
|
286
|
-
response
|
|
287
|
-
error
|
|
288
|
-
metadata
|
|
289
|
-
timestamp
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
"""
|
|
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
|
|
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(
|
|
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:
|