django-cfg 1.5.1__py3-none-any.whl → 1.5.3__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/dashboard/TRANSACTION_FIX.md +73 -0
- django_cfg/apps/dashboard/serializers/__init__.py +0 -12
- django_cfg/apps/dashboard/serializers/activity.py +1 -1
- django_cfg/apps/dashboard/services/__init__.py +0 -2
- django_cfg/apps/dashboard/services/charts_service.py +4 -3
- django_cfg/apps/dashboard/services/statistics_service.py +11 -2
- django_cfg/apps/dashboard/services/system_health_service.py +64 -106
- django_cfg/apps/dashboard/urls.py +0 -2
- django_cfg/apps/dashboard/views/__init__.py +0 -2
- django_cfg/apps/dashboard/views/commands_views.py +3 -6
- django_cfg/apps/dashboard/views/overview_views.py +14 -13
- django_cfg/apps/knowbase/apps.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +2 -3
- django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
- django_cfg/apps/rq/__init__.py +9 -0
- django_cfg/apps/rq/apps.py +80 -0
- django_cfg/apps/rq/management/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/rqscheduler.py +31 -0
- django_cfg/apps/rq/management/commands/rqstats.py +33 -0
- django_cfg/apps/rq/management/commands/rqworker.py +31 -0
- django_cfg/apps/rq/management/commands/rqworker_pool.py +27 -0
- django_cfg/apps/rq/serializers/__init__.py +40 -0
- django_cfg/apps/rq/serializers/health.py +60 -0
- django_cfg/apps/rq/serializers/job.py +100 -0
- django_cfg/apps/rq/serializers/queue.py +80 -0
- django_cfg/apps/rq/serializers/schedule.py +178 -0
- django_cfg/apps/rq/serializers/testing.py +139 -0
- django_cfg/apps/rq/serializers/worker.py +58 -0
- django_cfg/apps/rq/services/__init__.py +25 -0
- django_cfg/apps/rq/services/config_helper.py +233 -0
- django_cfg/apps/rq/services/models/README.md +417 -0
- django_cfg/apps/rq/services/models/__init__.py +30 -0
- django_cfg/apps/rq/services/models/event.py +123 -0
- django_cfg/apps/rq/services/models/job.py +99 -0
- django_cfg/apps/rq/services/models/queue.py +92 -0
- django_cfg/apps/rq/services/models/worker.py +104 -0
- django_cfg/apps/rq/services/rq_converters.py +183 -0
- django_cfg/apps/rq/tasks/__init__.py +23 -0
- django_cfg/apps/rq/tasks/demo_tasks.py +284 -0
- django_cfg/apps/rq/urls.py +54 -0
- django_cfg/apps/rq/views/__init__.py +19 -0
- django_cfg/apps/rq/views/jobs.py +882 -0
- django_cfg/apps/rq/views/monitoring.py +248 -0
- django_cfg/apps/rq/views/queues.py +261 -0
- django_cfg/apps/rq/views/schedule.py +400 -0
- django_cfg/apps/rq/views/testing.py +761 -0
- django_cfg/apps/rq/views/workers.py +195 -0
- django_cfg/apps/urls.py +6 -7
- django_cfg/core/base/config_model.py +10 -26
- django_cfg/core/builders/apps_builder.py +4 -11
- django_cfg/core/generation/integration_generators/__init__.py +3 -6
- django_cfg/core/generation/integration_generators/django_rq.py +80 -0
- django_cfg/core/generation/orchestrator.py +9 -19
- django_cfg/core/integration/display/startup.py +6 -20
- django_cfg/mixins/__init__.py +2 -0
- django_cfg/mixins/superadmin_api.py +59 -0
- django_cfg/models/__init__.py +3 -3
- django_cfg/models/django/__init__.py +3 -3
- django_cfg/models/django/django_rq.py +621 -0
- django_cfg/models/django/revolution_legacy.py +1 -1
- django_cfg/modules/base.py +4 -6
- django_cfg/modules/django_admin/config/background_task_config.py +4 -4
- django_cfg/modules/django_admin/utils/html/composition.py +9 -2
- django_cfg/modules/django_unfold/navigation.py +1 -26
- django_cfg/pyproject.toml +4 -4
- django_cfg/registry/core.py +4 -7
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/constance/includes/results_list.html +73 -0
- django_cfg/templates/admin/index.html +187 -62
- django_cfg/templatetags/django_cfg.py +61 -1
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/METADATA +5 -6
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/RECORD +77 -82
- django_cfg/apps/dashboard/permissions.py +0 -48
- django_cfg/apps/dashboard/serializers/django_q2.py +0 -50
- django_cfg/apps/dashboard/services/django_q2_service.py +0 -159
- django_cfg/apps/dashboard/views/django_q2_views.py +0 -79
- django_cfg/apps/tasks/__init__.py +0 -64
- django_cfg/apps/tasks/admin/__init__.py +0 -4
- django_cfg/apps/tasks/admin/config.py +0 -98
- django_cfg/apps/tasks/admin/task_log.py +0 -238
- django_cfg/apps/tasks/apps.py +0 -15
- django_cfg/apps/tasks/filters/__init__.py +0 -10
- django_cfg/apps/tasks/filters/task_log.py +0 -121
- django_cfg/apps/tasks/migrations/0001_initial.py +0 -196
- django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +0 -16
- django_cfg/apps/tasks/migrations/__init__.py +0 -0
- django_cfg/apps/tasks/models/__init__.py +0 -4
- django_cfg/apps/tasks/models/task_log.py +0 -246
- django_cfg/apps/tasks/serializers/__init__.py +0 -28
- django_cfg/apps/tasks/serializers/task_log.py +0 -249
- django_cfg/apps/tasks/services/__init__.py +0 -10
- django_cfg/apps/tasks/services/client/__init__.py +0 -7
- django_cfg/apps/tasks/services/client/client.py +0 -234
- django_cfg/apps/tasks/services/config_helper.py +0 -63
- django_cfg/apps/tasks/services/sync.py +0 -204
- django_cfg/apps/tasks/urls.py +0 -16
- django_cfg/apps/tasks/views/__init__.py +0 -10
- django_cfg/apps/tasks/views/task_log.py +0 -41
- django_cfg/apps/tasks/views/task_log_base.py +0 -41
- django_cfg/apps/tasks/views/task_log_overview.py +0 -100
- django_cfg/apps/tasks/views/task_log_related.py +0 -41
- django_cfg/apps/tasks/views/task_log_stats.py +0 -91
- django_cfg/apps/tasks/views/task_log_timeline.py +0 -81
- django_cfg/core/generation/integration_generators/django_q2.py +0 -133
- django_cfg/core/generation/integration_generators/tasks.py +0 -88
- django_cfg/models/django/django_q2.py +0 -514
- django_cfg/models/tasks/__init__.py +0 -49
- django_cfg/models/tasks/backends.py +0 -122
- django_cfg/models/tasks/config.py +0 -209
- django_cfg/models/tasks/utils.py +0 -162
- django_cfg/modules/django_q2/README.md +0 -140
- django_cfg/modules/django_q2/__init__.py +0 -8
- django_cfg/modules/django_q2/apps.py +0 -107
- django_cfg/modules/django_q2/management/__init__.py +0 -0
- django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
- django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +0 -74
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django-RQ Monitoring ViewSet.
|
|
3
|
+
|
|
4
|
+
Provides REST API endpoints for RQ health checks and configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from django_cfg.mixins import AdminAPIMixin
|
|
10
|
+
from django_cfg.modules.django_logging import get_logger
|
|
11
|
+
from drf_spectacular.utils import 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 import HealthCheckSerializer, RQConfigSerializer
|
|
17
|
+
|
|
18
|
+
logger = get_logger("rq.monitoring")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RQMonitorViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
22
|
+
"""
|
|
23
|
+
ViewSet for RQ monitoring and health checks.
|
|
24
|
+
|
|
25
|
+
Provides endpoints for:
|
|
26
|
+
- Health check (cluster status)
|
|
27
|
+
- Configuration view
|
|
28
|
+
- Prometheus metrics
|
|
29
|
+
|
|
30
|
+
Requires admin authentication (JWT, Session, or Basic Auth).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@extend_schema(
|
|
34
|
+
tags=["RQ Monitoring"],
|
|
35
|
+
summary="Health check",
|
|
36
|
+
description="Returns RQ cluster health status including worker count and queue status.",
|
|
37
|
+
responses={
|
|
38
|
+
200: HealthCheckSerializer,
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
@action(detail=False, methods=["get"], url_path="health")
|
|
42
|
+
def health(self, request):
|
|
43
|
+
"""Health check endpoint."""
|
|
44
|
+
try:
|
|
45
|
+
import django_rq
|
|
46
|
+
from rq import Worker
|
|
47
|
+
|
|
48
|
+
# Check Redis connection
|
|
49
|
+
redis_connected = True
|
|
50
|
+
worker_count = 0
|
|
51
|
+
total_jobs = 0
|
|
52
|
+
queue_count = 0
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
# Get all queues
|
|
56
|
+
from django.conf import settings
|
|
57
|
+
|
|
58
|
+
queue_names = settings.RQ_QUEUES.keys() if hasattr(settings, 'RQ_QUEUES') else []
|
|
59
|
+
queue_count = len(queue_names)
|
|
60
|
+
|
|
61
|
+
# Count workers per queue (use set to avoid duplicates)
|
|
62
|
+
worker_names = set()
|
|
63
|
+
for queue_name in queue_names:
|
|
64
|
+
try:
|
|
65
|
+
queue = django_rq.get_queue(queue_name)
|
|
66
|
+
# Get workers for this queue's connection
|
|
67
|
+
queue_workers = Worker.all(connection=queue.connection)
|
|
68
|
+
worker_names.update(w.name for w in queue_workers)
|
|
69
|
+
# Count jobs
|
|
70
|
+
total_jobs += queue.count
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.debug(f"Failed to get queue {queue_name}: {e}")
|
|
73
|
+
|
|
74
|
+
worker_count = len(worker_names)
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Redis connection error: {e}", exc_info=True)
|
|
78
|
+
redis_connected = False
|
|
79
|
+
|
|
80
|
+
# Determine health status
|
|
81
|
+
if not redis_connected:
|
|
82
|
+
health_status = "unhealthy"
|
|
83
|
+
elif worker_count == 0:
|
|
84
|
+
health_status = "degraded"
|
|
85
|
+
else:
|
|
86
|
+
health_status = "healthy"
|
|
87
|
+
|
|
88
|
+
health_data = {
|
|
89
|
+
"status": health_status,
|
|
90
|
+
"worker_count": worker_count,
|
|
91
|
+
"queue_count": queue_count,
|
|
92
|
+
"total_jobs": total_jobs,
|
|
93
|
+
"timestamp": datetime.now().isoformat(),
|
|
94
|
+
"enabled": True,
|
|
95
|
+
"redis_connected": redis_connected,
|
|
96
|
+
"wrapper_url": "", # Empty for now, can be configured later
|
|
97
|
+
"has_api_key": False, # No API key required for RQ
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
serializer = HealthCheckSerializer(data=health_data)
|
|
101
|
+
serializer.is_valid(raise_exception=True)
|
|
102
|
+
return Response(serializer.data)
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.error(f"Health check error: {e}", exc_info=True)
|
|
106
|
+
return Response(
|
|
107
|
+
{"error": "Internal server error"},
|
|
108
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@extend_schema(
|
|
112
|
+
tags=["RQ Monitoring"],
|
|
113
|
+
summary="Get RQ configuration",
|
|
114
|
+
description="Returns current RQ configuration from django-cfg.",
|
|
115
|
+
responses={
|
|
116
|
+
200: RQConfigSerializer,
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
@action(detail=False, methods=["get"], url_path="config")
|
|
120
|
+
def config(self, request):
|
|
121
|
+
"""Get RQ configuration."""
|
|
122
|
+
try:
|
|
123
|
+
from django.conf import settings
|
|
124
|
+
from ..services import get_rq_config
|
|
125
|
+
|
|
126
|
+
# Get configuration
|
|
127
|
+
queues = {}
|
|
128
|
+
if hasattr(settings, 'RQ_QUEUES'):
|
|
129
|
+
# Sanitize queue config (hide passwords)
|
|
130
|
+
for name, config in settings.RQ_QUEUES.items():
|
|
131
|
+
queues[name] = {
|
|
132
|
+
k: v if k not in ('PASSWORD', 'password') else '***'
|
|
133
|
+
for k, v in config.items()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async_mode = True
|
|
137
|
+
show_admin_link = getattr(settings, 'RQ_SHOW_ADMIN_LINK', True)
|
|
138
|
+
api_token_configured = bool(getattr(settings, 'RQ_API_TOKEN', None))
|
|
139
|
+
|
|
140
|
+
# Check prometheus
|
|
141
|
+
prometheus_enabled = True
|
|
142
|
+
try:
|
|
143
|
+
import prometheus_client # noqa: F401
|
|
144
|
+
except ImportError:
|
|
145
|
+
prometheus_enabled = False
|
|
146
|
+
|
|
147
|
+
# Get scheduled tasks from django-cfg config
|
|
148
|
+
schedules = []
|
|
149
|
+
rq_config = get_rq_config()
|
|
150
|
+
if rq_config and hasattr(rq_config, 'schedules'):
|
|
151
|
+
for schedule in rq_config.schedules:
|
|
152
|
+
schedules.append({
|
|
153
|
+
"func": schedule.func,
|
|
154
|
+
"queue": schedule.queue,
|
|
155
|
+
"cron": schedule.cron,
|
|
156
|
+
"interval": schedule.interval,
|
|
157
|
+
"scheduled_time": schedule.scheduled_time,
|
|
158
|
+
"description": schedule.description,
|
|
159
|
+
"timeout": schedule.timeout,
|
|
160
|
+
"result_ttl": schedule.result_ttl,
|
|
161
|
+
"repeat": schedule.repeat,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
config_data = {
|
|
165
|
+
"enabled": True,
|
|
166
|
+
"queues": queues,
|
|
167
|
+
"async_mode": async_mode,
|
|
168
|
+
"show_admin_link": show_admin_link,
|
|
169
|
+
"prometheus_enabled": prometheus_enabled,
|
|
170
|
+
"api_token_configured": api_token_configured,
|
|
171
|
+
"schedules": schedules,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
serializer = RQConfigSerializer(data=config_data)
|
|
175
|
+
serializer.is_valid(raise_exception=True)
|
|
176
|
+
return Response(serializer.data)
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"Config error: {e}", exc_info=True)
|
|
180
|
+
return Response(
|
|
181
|
+
{"error": "Internal server error"},
|
|
182
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@extend_schema(
|
|
186
|
+
tags=["RQ Monitoring"],
|
|
187
|
+
summary="Prometheus metrics",
|
|
188
|
+
description="Returns Prometheus metrics for RQ queues and workers.",
|
|
189
|
+
responses={
|
|
190
|
+
200: {"description": "Prometheus metrics in text format"},
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
@action(detail=False, methods=["get"], url_path="metrics")
|
|
194
|
+
def metrics(self, request):
|
|
195
|
+
"""
|
|
196
|
+
Prometheus metrics endpoint.
|
|
197
|
+
|
|
198
|
+
Uses django-rq's built-in RQCollector for comprehensive metrics:
|
|
199
|
+
- rq_workers: Worker count by name, state, and queues
|
|
200
|
+
- rq_job_successful_total: Successful job count per worker
|
|
201
|
+
- rq_job_failed_total: Failed job count per worker
|
|
202
|
+
- rq_working_seconds_total: Total working time per worker
|
|
203
|
+
- rq_jobs: Job count by queue and status (queued, started, finished, failed, deferred, scheduled)
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
# Try to import prometheus_client and RQCollector
|
|
207
|
+
try:
|
|
208
|
+
from prometheus_client import CollectorRegistry
|
|
209
|
+
from prometheus_client.exposition import choose_encoder
|
|
210
|
+
except ImportError:
|
|
211
|
+
return Response(
|
|
212
|
+
{"error": "prometheus_client not installed. Install with: pip install django-rq[prometheus]"},
|
|
213
|
+
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
from django_rq.contrib.prometheus import RQCollector
|
|
218
|
+
except ImportError:
|
|
219
|
+
return Response(
|
|
220
|
+
{"error": "RQCollector not available. Ensure prometheus_client is installed."},
|
|
221
|
+
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Create registry and register RQCollector
|
|
225
|
+
registry = CollectorRegistry(auto_describe=True)
|
|
226
|
+
registry.register(RQCollector())
|
|
227
|
+
|
|
228
|
+
# Choose encoder based on Accept header
|
|
229
|
+
encoder, content_type = choose_encoder(request.META.get('HTTP_ACCEPT', ''))
|
|
230
|
+
|
|
231
|
+
# Support filtering by metric name (Prometheus query param)
|
|
232
|
+
if 'name[]' in request.GET:
|
|
233
|
+
registry = registry.restricted_registry(request.GET.getlist('name[]'))
|
|
234
|
+
|
|
235
|
+
# Generate and return metrics
|
|
236
|
+
metrics_output = encoder(registry)
|
|
237
|
+
|
|
238
|
+
return Response(
|
|
239
|
+
metrics_output,
|
|
240
|
+
content_type=content_type,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"Metrics error: {e}", exc_info=True)
|
|
245
|
+
return Response(
|
|
246
|
+
{"error": "Internal server error"},
|
|
247
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
248
|
+
)
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django-RQ Queue Management ViewSet.
|
|
3
|
+
|
|
4
|
+
Provides REST API endpoints for managing RQ queues.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django_cfg.mixins import AdminAPIMixin
|
|
8
|
+
from django_cfg.modules.django_logging import get_logger
|
|
9
|
+
from drf_spectacular.types import OpenApiTypes
|
|
10
|
+
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
|
11
|
+
from rest_framework import status, viewsets
|
|
12
|
+
from rest_framework.decorators import action
|
|
13
|
+
from rest_framework.response import Response
|
|
14
|
+
|
|
15
|
+
from ..serializers import QueueStatsSerializer, QueueDetailSerializer
|
|
16
|
+
from ..services import queue_to_model
|
|
17
|
+
|
|
18
|
+
logger = get_logger("rq.queues")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class QueueViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
22
|
+
"""
|
|
23
|
+
ViewSet for RQ queue management.
|
|
24
|
+
|
|
25
|
+
Provides endpoints for:
|
|
26
|
+
- Listing all queues with statistics
|
|
27
|
+
- Getting detailed queue information
|
|
28
|
+
- Emptying queues
|
|
29
|
+
- Getting queue job lists
|
|
30
|
+
|
|
31
|
+
Requires admin authentication (JWT, Session, or Basic Auth).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@extend_schema(
|
|
35
|
+
tags=["RQ Queues"],
|
|
36
|
+
summary="List all queues",
|
|
37
|
+
description="Returns list of all configured RQ queues with statistics. Supports filtering by queue name.",
|
|
38
|
+
parameters=[
|
|
39
|
+
OpenApiParameter(
|
|
40
|
+
name="name",
|
|
41
|
+
type=str,
|
|
42
|
+
location=OpenApiParameter.QUERY,
|
|
43
|
+
required=False,
|
|
44
|
+
description="Filter by queue name (exact match or substring)",
|
|
45
|
+
),
|
|
46
|
+
],
|
|
47
|
+
responses={
|
|
48
|
+
200: QueueStatsSerializer(many=True),
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
def list(self, request):
|
|
52
|
+
"""List all queues with statistics and optional filtering."""
|
|
53
|
+
try:
|
|
54
|
+
import django_rq
|
|
55
|
+
from django.conf import settings
|
|
56
|
+
|
|
57
|
+
if not hasattr(settings, 'RQ_QUEUES'):
|
|
58
|
+
return Response([])
|
|
59
|
+
|
|
60
|
+
# Get query params for filtering
|
|
61
|
+
name_filter = request.query_params.get('name')
|
|
62
|
+
|
|
63
|
+
queues_data = []
|
|
64
|
+
|
|
65
|
+
for queue_name in settings.RQ_QUEUES.keys():
|
|
66
|
+
try:
|
|
67
|
+
# Apply name filter
|
|
68
|
+
if name_filter and name_filter.lower() not in queue_name.lower():
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
queue = django_rq.get_queue(queue_name)
|
|
72
|
+
|
|
73
|
+
# Convert RQ Queue to Pydantic model
|
|
74
|
+
queue_model = queue_to_model(queue, queue_name)
|
|
75
|
+
|
|
76
|
+
# Convert to dict for DRF serializer (only stats fields)
|
|
77
|
+
queue_dict = {
|
|
78
|
+
"name": queue_model.name,
|
|
79
|
+
"count": queue_model.count,
|
|
80
|
+
"queued_jobs": queue_model.queued_jobs,
|
|
81
|
+
"started_jobs": queue_model.started_jobs,
|
|
82
|
+
"finished_jobs": queue_model.finished_jobs,
|
|
83
|
+
"failed_jobs": queue_model.failed_jobs,
|
|
84
|
+
"deferred_jobs": queue_model.deferred_jobs,
|
|
85
|
+
"scheduled_jobs": queue_model.scheduled_jobs,
|
|
86
|
+
"workers": queue_model.workers,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
serializer = QueueStatsSerializer(data=queue_dict)
|
|
90
|
+
serializer.is_valid(raise_exception=True)
|
|
91
|
+
queues_data.append(serializer.data)
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Failed to get queue {queue_name}: {e}", exc_info=True)
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
return Response(queues_data)
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Queue list error: {e}", exc_info=True)
|
|
101
|
+
return Response(
|
|
102
|
+
{"error": "Internal server error"},
|
|
103
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@extend_schema(
|
|
107
|
+
tags=["RQ Queues"],
|
|
108
|
+
summary="Get queue details",
|
|
109
|
+
description="Returns detailed information about a specific queue.",
|
|
110
|
+
responses={
|
|
111
|
+
200: QueueDetailSerializer,
|
|
112
|
+
404: {"description": "Queue not found"},
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
def retrieve(self, request, pk=None):
|
|
116
|
+
"""Get detailed queue information."""
|
|
117
|
+
try:
|
|
118
|
+
import django_rq
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
queue = django_rq.get_queue(pk)
|
|
122
|
+
except Exception:
|
|
123
|
+
return Response(
|
|
124
|
+
{"error": f"Queue {pk} not found"},
|
|
125
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Convert RQ Queue to Pydantic model
|
|
129
|
+
queue_model = queue_to_model(queue, pk)
|
|
130
|
+
|
|
131
|
+
# Convert to dict for DRF serializer
|
|
132
|
+
queue_dict = {
|
|
133
|
+
"name": queue_model.name,
|
|
134
|
+
"count": queue_model.count,
|
|
135
|
+
"queued_jobs": queue_model.queued_jobs,
|
|
136
|
+
"started_jobs": queue_model.started_jobs,
|
|
137
|
+
"finished_jobs": queue_model.finished_jobs,
|
|
138
|
+
"failed_jobs": queue_model.failed_jobs,
|
|
139
|
+
"deferred_jobs": queue_model.deferred_jobs,
|
|
140
|
+
"scheduled_jobs": queue_model.scheduled_jobs,
|
|
141
|
+
"workers": queue_model.workers,
|
|
142
|
+
"oldest_job_timestamp": queue_model.oldest_job_timestamp,
|
|
143
|
+
"connection_kwargs": {
|
|
144
|
+
'host': queue_model.connection_host,
|
|
145
|
+
'port': queue_model.connection_port,
|
|
146
|
+
'db': queue_model.connection_db,
|
|
147
|
+
},
|
|
148
|
+
"is_async": queue_model.is_async,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
serializer = QueueDetailSerializer(data=queue_dict)
|
|
152
|
+
serializer.is_valid(raise_exception=True)
|
|
153
|
+
return Response(serializer.data)
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.error(f"Queue detail error: {e}", exc_info=True)
|
|
157
|
+
return Response(
|
|
158
|
+
{"error": "Internal server error"},
|
|
159
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@extend_schema(
|
|
163
|
+
tags=["RQ Queues"],
|
|
164
|
+
summary="Empty queue",
|
|
165
|
+
description="Removes all jobs from the specified queue.",
|
|
166
|
+
responses={
|
|
167
|
+
200: {"description": "Queue emptied successfully"},
|
|
168
|
+
404: {"description": "Queue not found"},
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
@action(detail=True, methods=["post"], url_path="empty")
|
|
172
|
+
def empty(self, request, pk=None):
|
|
173
|
+
"""Empty queue (remove all jobs)."""
|
|
174
|
+
try:
|
|
175
|
+
import django_rq
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
queue = django_rq.get_queue(pk)
|
|
179
|
+
except Exception:
|
|
180
|
+
return Response(
|
|
181
|
+
{"error": f"Queue {pk} not found"},
|
|
182
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
job_count = queue.count
|
|
186
|
+
queue.empty()
|
|
187
|
+
|
|
188
|
+
return Response({
|
|
189
|
+
"success": True,
|
|
190
|
+
"message": f"Emptied queue '{pk}', removed {job_count} jobs",
|
|
191
|
+
"queue": pk,
|
|
192
|
+
"removed_jobs": job_count,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error(f"Queue empty error: {e}", exc_info=True)
|
|
197
|
+
return Response(
|
|
198
|
+
{"error": "Internal server error"},
|
|
199
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
@extend_schema(
|
|
203
|
+
tags=["RQ Queues"],
|
|
204
|
+
summary="Get queue jobs",
|
|
205
|
+
description="Returns list of job IDs in the queue.",
|
|
206
|
+
parameters=[
|
|
207
|
+
OpenApiParameter(
|
|
208
|
+
name="limit",
|
|
209
|
+
type=OpenApiTypes.INT,
|
|
210
|
+
location=OpenApiParameter.QUERY,
|
|
211
|
+
description="Number of jobs to return (default: 100)",
|
|
212
|
+
required=False,
|
|
213
|
+
),
|
|
214
|
+
OpenApiParameter(
|
|
215
|
+
name="offset",
|
|
216
|
+
type=OpenApiTypes.INT,
|
|
217
|
+
location=OpenApiParameter.QUERY,
|
|
218
|
+
description="Offset for pagination (default: 0)",
|
|
219
|
+
required=False,
|
|
220
|
+
),
|
|
221
|
+
],
|
|
222
|
+
responses={
|
|
223
|
+
200: {"description": "List of job IDs"},
|
|
224
|
+
404: {"description": "Queue not found"},
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
@action(detail=True, methods=["get"], url_path="jobs")
|
|
228
|
+
def jobs(self, request, pk=None):
|
|
229
|
+
"""Get list of jobs in queue."""
|
|
230
|
+
try:
|
|
231
|
+
import django_rq
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
queue = django_rq.get_queue(pk)
|
|
235
|
+
except Exception:
|
|
236
|
+
return Response(
|
|
237
|
+
{"error": f"Queue {pk} not found"},
|
|
238
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Pagination
|
|
242
|
+
limit = min(int(request.query_params.get("limit", 100)), 500)
|
|
243
|
+
offset = int(request.query_params.get("offset", 0))
|
|
244
|
+
|
|
245
|
+
# Get job IDs
|
|
246
|
+
job_ids = queue.get_job_ids(offset, offset + limit)
|
|
247
|
+
|
|
248
|
+
return Response({
|
|
249
|
+
"queue": pk,
|
|
250
|
+
"total_jobs": queue.count,
|
|
251
|
+
"limit": limit,
|
|
252
|
+
"offset": offset,
|
|
253
|
+
"job_ids": job_ids,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.error(f"Queue jobs error: {e}", exc_info=True)
|
|
258
|
+
return Response(
|
|
259
|
+
{"error": "Internal server error"},
|
|
260
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
261
|
+
)
|