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,621 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django-RQ Configuration for django-cfg.
|
|
3
|
+
|
|
4
|
+
Type-safe configuration for django-rq with automatic Django settings generation
|
|
5
|
+
and support for scheduled tasks via rq-scheduler.
|
|
6
|
+
|
|
7
|
+
Django-RQ is a Redis-based task queue: https://github.com/rq/django-rq
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Type-safe queue and scheduler configuration
|
|
11
|
+
- Redis connection management (standard, Sentinel, SSL)
|
|
12
|
+
- Job timeout and TTL configuration
|
|
13
|
+
- Built-in Prometheus metrics support
|
|
14
|
+
- Exception handler configuration
|
|
15
|
+
- Admin interface with monitoring
|
|
16
|
+
- RQ Scheduler for cron-like scheduling
|
|
17
|
+
- High performance (10,000+ jobs/sec)
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
```python
|
|
21
|
+
from django_cfg.models.django.django_rq import DjangoRQConfig, RQQueueConfig
|
|
22
|
+
|
|
23
|
+
django_rq_config = DjangoRQConfig(
|
|
24
|
+
enabled=True,
|
|
25
|
+
queues=[
|
|
26
|
+
RQQueueConfig(
|
|
27
|
+
queue="default",
|
|
28
|
+
host="localhost",
|
|
29
|
+
port=6379,
|
|
30
|
+
db=0,
|
|
31
|
+
default_timeout=360,
|
|
32
|
+
),
|
|
33
|
+
RQQueueConfig(
|
|
34
|
+
queue="high",
|
|
35
|
+
host="localhost",
|
|
36
|
+
port=6379,
|
|
37
|
+
db=0,
|
|
38
|
+
default_timeout=180,
|
|
39
|
+
),
|
|
40
|
+
],
|
|
41
|
+
show_admin_link=True,
|
|
42
|
+
prometheus_enabled=True,
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Scheduler Support:
|
|
47
|
+
Use rq-scheduler for cron-like scheduled tasks:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install rq-scheduler
|
|
51
|
+
python manage.py rqscheduler
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import django_rq
|
|
56
|
+
scheduler = django_rq.get_scheduler('default')
|
|
57
|
+
|
|
58
|
+
# Schedule job for specific time
|
|
59
|
+
from datetime import datetime
|
|
60
|
+
scheduler.enqueue_at(datetime(2025, 12, 31, 23, 59), my_task)
|
|
61
|
+
|
|
62
|
+
# Schedule job with interval
|
|
63
|
+
scheduler.schedule(
|
|
64
|
+
scheduled_time=datetime.utcnow(),
|
|
65
|
+
func=my_task,
|
|
66
|
+
interval=60, # Every 60 seconds
|
|
67
|
+
repeat=None, # Repeat forever
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Cron-style scheduling
|
|
71
|
+
scheduler.cron(
|
|
72
|
+
"0 0 * * *", # Every day at midnight
|
|
73
|
+
func=my_task,
|
|
74
|
+
queue_name='default'
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
from typing import Any, Dict, List, Optional
|
|
80
|
+
|
|
81
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RQQueueConfig(BaseModel):
|
|
85
|
+
"""
|
|
86
|
+
Configuration for a single RQ queue.
|
|
87
|
+
|
|
88
|
+
Supports standard Redis, Redis Sentinel, and SSL connections.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
model_config = ConfigDict(
|
|
92
|
+
validate_assignment=True,
|
|
93
|
+
extra="forbid",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Queue name
|
|
97
|
+
queue: str = Field(
|
|
98
|
+
...,
|
|
99
|
+
min_length=1,
|
|
100
|
+
max_length=100,
|
|
101
|
+
pattern=r'^[a-zA-Z0-9_-]+$',
|
|
102
|
+
description="Queue name (alphanumeric, hyphens, underscores)",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Redis URL (alternative to host/port/db)
|
|
106
|
+
url: Optional[str] = Field(
|
|
107
|
+
default=None,
|
|
108
|
+
description="Redis URL (redis://localhost:6379/0). If provided, overrides host/port/db.",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Standard Redis connection
|
|
112
|
+
host: str = Field(
|
|
113
|
+
default="localhost",
|
|
114
|
+
description="Redis host",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
port: int = Field(
|
|
118
|
+
default=6379,
|
|
119
|
+
ge=1,
|
|
120
|
+
le=65535,
|
|
121
|
+
description="Redis port",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
db: int = Field(
|
|
125
|
+
default=0,
|
|
126
|
+
ge=0,
|
|
127
|
+
le=15,
|
|
128
|
+
description="Redis database number (0-15)",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
username: Optional[str] = Field(
|
|
132
|
+
default=None,
|
|
133
|
+
description="Redis username (Redis 6+)",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
password: Optional[str] = Field(
|
|
137
|
+
default=None,
|
|
138
|
+
description="Redis password",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Job defaults
|
|
142
|
+
default_timeout: int = Field(
|
|
143
|
+
default=360,
|
|
144
|
+
ge=1,
|
|
145
|
+
description="Default job timeout in seconds",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
default_result_ttl: int = Field(
|
|
149
|
+
default=800,
|
|
150
|
+
ge=0,
|
|
151
|
+
description="Default result TTL in seconds (0 = no expiry)",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Redis Sentinel support
|
|
155
|
+
sentinels: Optional[List[tuple[str, int]]] = Field(
|
|
156
|
+
default=None,
|
|
157
|
+
description="List of Sentinel (host, port) tuples",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
master_name: Optional[str] = Field(
|
|
161
|
+
default=None,
|
|
162
|
+
description="Redis Sentinel master name",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
socket_timeout: Optional[float] = Field(
|
|
166
|
+
default=None,
|
|
167
|
+
ge=0.1,
|
|
168
|
+
description="Redis socket timeout in seconds",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Advanced connection options
|
|
172
|
+
connection_kwargs: Dict[str, Any] = Field(
|
|
173
|
+
default_factory=dict,
|
|
174
|
+
description="Additional Redis connection arguments (e.g., ssl=True)",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
redis_client_kwargs: Dict[str, Any] = Field(
|
|
178
|
+
default_factory=dict,
|
|
179
|
+
description="Additional Redis client arguments (e.g., ssl_cert_reqs)",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
sentinel_kwargs: Dict[str, Any] = Field(
|
|
183
|
+
default_factory=dict,
|
|
184
|
+
description="Sentinel-specific connection arguments (username/password for Sentinel auth)",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def to_django_rq_format(self, redis_url: Optional[str] = None) -> Dict[str, Any]:
|
|
188
|
+
"""
|
|
189
|
+
Convert to Django-RQ queue configuration format.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
redis_url: Redis URL from parent DjangoConfig (if available)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Dictionary for RQ_QUEUES[queue_name] in settings.py
|
|
196
|
+
"""
|
|
197
|
+
config: Dict[str, Any] = {}
|
|
198
|
+
|
|
199
|
+
# Priority: url field > redis_url from parent > Sentinel > host/port/db
|
|
200
|
+
if self.url:
|
|
201
|
+
config["URL"] = self.url
|
|
202
|
+
elif redis_url:
|
|
203
|
+
# Use redis_url from parent DjangoConfig
|
|
204
|
+
config["URL"] = redis_url
|
|
205
|
+
# Use Sentinel if configured
|
|
206
|
+
elif self.sentinels and self.master_name:
|
|
207
|
+
config["SENTINELS"] = self.sentinels
|
|
208
|
+
config["MASTER_NAME"] = self.master_name
|
|
209
|
+
config["DB"] = self.db
|
|
210
|
+
|
|
211
|
+
if self.socket_timeout:
|
|
212
|
+
config["SOCKET_TIMEOUT"] = self.socket_timeout
|
|
213
|
+
|
|
214
|
+
if self.connection_kwargs:
|
|
215
|
+
config["CONNECTION_KWARGS"] = self.connection_kwargs
|
|
216
|
+
|
|
217
|
+
if self.sentinel_kwargs:
|
|
218
|
+
config["SENTINEL_KWARGS"] = self.sentinel_kwargs
|
|
219
|
+
|
|
220
|
+
else:
|
|
221
|
+
# Standard Redis connection
|
|
222
|
+
config["HOST"] = self.host
|
|
223
|
+
config["PORT"] = self.port
|
|
224
|
+
config["DB"] = self.db
|
|
225
|
+
|
|
226
|
+
if self.redis_client_kwargs:
|
|
227
|
+
config["REDIS_CLIENT_KWARGS"] = self.redis_client_kwargs
|
|
228
|
+
|
|
229
|
+
# Common options
|
|
230
|
+
if self.username:
|
|
231
|
+
config["USERNAME"] = self.username
|
|
232
|
+
|
|
233
|
+
if self.password:
|
|
234
|
+
config["PASSWORD"] = self.password
|
|
235
|
+
|
|
236
|
+
config["DEFAULT_TIMEOUT"] = self.default_timeout
|
|
237
|
+
config["DEFAULT_RESULT_TTL"] = self.default_result_ttl
|
|
238
|
+
|
|
239
|
+
return config
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class DjangoRQConfig(BaseModel):
|
|
243
|
+
"""
|
|
244
|
+
Complete Django-RQ configuration container.
|
|
245
|
+
|
|
246
|
+
Integrates with django-rq for Redis-based task queuing with high performance.
|
|
247
|
+
Automatically adds django_rq to INSTALLED_APPS when enabled.
|
|
248
|
+
|
|
249
|
+
Installation:
|
|
250
|
+
```bash
|
|
251
|
+
pip install django-rq rq-scheduler
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Running workers:
|
|
255
|
+
```bash
|
|
256
|
+
# Start worker for default queue
|
|
257
|
+
python manage.py rqworker default
|
|
258
|
+
|
|
259
|
+
# Start worker for multiple queues (priority order)
|
|
260
|
+
python manage.py rqworker high default low
|
|
261
|
+
|
|
262
|
+
# Start scheduler for scheduled tasks
|
|
263
|
+
python manage.py rqscheduler
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Admin interface:
|
|
267
|
+
- Visit /django-rq/ to view queues, workers, and jobs
|
|
268
|
+
- Monitor job execution, failures, and performance
|
|
269
|
+
- Manually requeue or delete jobs
|
|
270
|
+
- View worker statistics
|
|
271
|
+
|
|
272
|
+
Prometheus metrics:
|
|
273
|
+
When prometheus_enabled=True, metrics are exposed at /django-rq/metrics/
|
|
274
|
+
- rq_jobs_total{queue, status}
|
|
275
|
+
- rq_job_duration_seconds{queue}
|
|
276
|
+
- rq_workers_total{queue}
|
|
277
|
+
- rq_queue_length{queue}
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
model_config = ConfigDict(
|
|
281
|
+
validate_assignment=True,
|
|
282
|
+
extra="forbid",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
enabled: bool = Field(
|
|
286
|
+
default=True,
|
|
287
|
+
description="Enable Django-RQ (auto-adds django_rq to INSTALLED_APPS)",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
queues: List[RQQueueConfig] = Field(
|
|
291
|
+
default_factory=lambda: [
|
|
292
|
+
RQQueueConfig(queue="default"),
|
|
293
|
+
],
|
|
294
|
+
description="Queue configurations (at least 'default' required)",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Admin interface
|
|
298
|
+
show_admin_link: bool = Field(
|
|
299
|
+
default=True,
|
|
300
|
+
description="Show link to RQ admin in Django admin",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Exception handlers
|
|
304
|
+
exception_handlers: List[str] = Field(
|
|
305
|
+
default_factory=list,
|
|
306
|
+
description="List of exception handler function paths (e.g., 'myapp.handlers.log_exception')",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# API access
|
|
310
|
+
api_token: Optional[str] = Field(
|
|
311
|
+
default=None,
|
|
312
|
+
description="API token for statistics endpoint authentication",
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Prometheus metrics
|
|
316
|
+
prometheus_enabled: bool = Field(
|
|
317
|
+
default=True,
|
|
318
|
+
description="Enable Prometheus metrics at /django-rq/metrics/",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# RQ Scheduler - scheduled jobs configuration
|
|
322
|
+
schedules: List["RQScheduleConfig"] = Field(
|
|
323
|
+
default_factory=list,
|
|
324
|
+
description="Scheduled jobs for rq-scheduler (cron-style, interval, or one-time)",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
@field_validator("queues")
|
|
328
|
+
@classmethod
|
|
329
|
+
def validate_unique_queue_names(cls, queues: List[RQQueueConfig]) -> List[RQQueueConfig]:
|
|
330
|
+
"""Validate that all queue names are unique."""
|
|
331
|
+
queue_names = [q.queue for q in queues]
|
|
332
|
+
if len(queue_names) != len(set(queue_names)):
|
|
333
|
+
duplicates = [name for name in queue_names if queue_names.count(name) > 1]
|
|
334
|
+
raise ValueError(
|
|
335
|
+
f"Duplicate queue names found: {set(duplicates)}. "
|
|
336
|
+
"Each queue must have a unique name."
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Ensure 'default' queue exists
|
|
340
|
+
if 'default' not in queue_names:
|
|
341
|
+
raise ValueError(
|
|
342
|
+
"A queue named 'default' is required. "
|
|
343
|
+
"Add RQQueueConfig(queue='default', ...) to the queues list."
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return queues
|
|
347
|
+
|
|
348
|
+
def to_django_settings(self, parent_config: Optional[Any] = None) -> Dict[str, Any]:
|
|
349
|
+
"""
|
|
350
|
+
Convert to Django settings dictionary.
|
|
351
|
+
|
|
352
|
+
Generates RQ_QUEUES and related configuration for Django-RQ.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
parent_config: Optional parent DjangoConfig for accessing redis_url
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Dictionary with RQ_QUEUES, RQ_SHOW_ADMIN_LINK, etc.
|
|
359
|
+
"""
|
|
360
|
+
if not self.enabled:
|
|
361
|
+
return {}
|
|
362
|
+
|
|
363
|
+
settings: Dict[str, Any] = {}
|
|
364
|
+
|
|
365
|
+
# Get redis_url from parent config if available
|
|
366
|
+
redis_url = None
|
|
367
|
+
if parent_config and hasattr(parent_config, 'redis_url'):
|
|
368
|
+
redis_url = parent_config.redis_url
|
|
369
|
+
|
|
370
|
+
# Generate RQ_QUEUES configuration from list
|
|
371
|
+
rq_queues = {}
|
|
372
|
+
for queue_config in self.queues:
|
|
373
|
+
rq_queues[queue_config.queue] = queue_config.to_django_rq_format(redis_url=redis_url)
|
|
374
|
+
|
|
375
|
+
settings["RQ_QUEUES"] = rq_queues
|
|
376
|
+
settings["RQ_SHOW_ADMIN_LINK"] = self.show_admin_link
|
|
377
|
+
|
|
378
|
+
if self.exception_handlers:
|
|
379
|
+
settings["RQ_EXCEPTION_HANDLERS"] = self.exception_handlers
|
|
380
|
+
|
|
381
|
+
if self.api_token:
|
|
382
|
+
settings["RQ_API_TOKEN"] = self.api_token
|
|
383
|
+
|
|
384
|
+
return settings
|
|
385
|
+
|
|
386
|
+
def get_queue_names(self) -> List[str]:
|
|
387
|
+
"""Get list of configured queue names."""
|
|
388
|
+
return [q.queue for q in self.queues]
|
|
389
|
+
|
|
390
|
+
def get_queue_config(self, queue_name: str) -> Optional[RQQueueConfig]:
|
|
391
|
+
"""Get configuration for specific queue."""
|
|
392
|
+
for queue in self.queues:
|
|
393
|
+
if queue.queue == queue_name:
|
|
394
|
+
return queue
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
def add_queue(self, config: RQQueueConfig) -> None:
|
|
398
|
+
"""
|
|
399
|
+
Add a new queue configuration.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
config: RQQueueConfig instance with queue name set
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
ValueError: If queue with this name already exists
|
|
406
|
+
"""
|
|
407
|
+
if config.queue in self.get_queue_names():
|
|
408
|
+
raise ValueError(f"Queue '{config.queue}' already exists")
|
|
409
|
+
self.queues.append(config)
|
|
410
|
+
|
|
411
|
+
def remove_queue(self, queue_name: str) -> bool:
|
|
412
|
+
"""
|
|
413
|
+
Remove a queue configuration.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
queue_name: Name of the queue to remove
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
True if queue was removed, False if not found
|
|
420
|
+
"""
|
|
421
|
+
for i, queue in enumerate(self.queues):
|
|
422
|
+
if queue.queue == queue_name:
|
|
423
|
+
self.queues.pop(i)
|
|
424
|
+
return True
|
|
425
|
+
return False
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class RQScheduleConfig(BaseModel):
|
|
429
|
+
"""
|
|
430
|
+
Configuration for RQ Scheduler scheduled job.
|
|
431
|
+
|
|
432
|
+
RQ Scheduler supports:
|
|
433
|
+
- Cron-style scheduling
|
|
434
|
+
- Interval-based scheduling
|
|
435
|
+
- One-time scheduled jobs
|
|
436
|
+
- Declarative task parameters (limit, verbosity, report_type, days, force)
|
|
437
|
+
|
|
438
|
+
Example:
|
|
439
|
+
```python
|
|
440
|
+
# Cron schedule with declarative parameters
|
|
441
|
+
RQScheduleConfig(
|
|
442
|
+
func="myapp.tasks.update_prices",
|
|
443
|
+
cron="*/5 * * * *", # Every 5 minutes
|
|
444
|
+
queue="default",
|
|
445
|
+
limit=50, # Type-safe field, automatically added to kwargs
|
|
446
|
+
verbosity=0, # Type-safe field, automatically added to kwargs
|
|
447
|
+
description="Update coin prices",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Interval schedule with declarative parameters
|
|
451
|
+
RQScheduleConfig(
|
|
452
|
+
func="myapp.tasks.cleanup",
|
|
453
|
+
interval=3600, # Every hour
|
|
454
|
+
queue="low",
|
|
455
|
+
days=7, # Type-safe field, automatically added to kwargs
|
|
456
|
+
force=True, # Type-safe field, automatically added to kwargs
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Traditional way (still works for custom parameters)
|
|
460
|
+
RQScheduleConfig(
|
|
461
|
+
func="myapp.tasks.my_task",
|
|
462
|
+
interval=60,
|
|
463
|
+
kwargs={"custom_param": "value"},
|
|
464
|
+
)
|
|
465
|
+
```
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
model_config = ConfigDict(
|
|
469
|
+
validate_assignment=True,
|
|
470
|
+
extra="forbid", # Strict validation - use declared fields only
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
func: str = Field(
|
|
474
|
+
...,
|
|
475
|
+
description="Function path (e.g., 'myapp.tasks.my_task')",
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Schedule type (one of: cron, interval, or scheduled_time)
|
|
479
|
+
cron: Optional[str] = Field(
|
|
480
|
+
default=None,
|
|
481
|
+
description="Cron expression (e.g., '0 0 * * *' for daily at midnight)",
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
interval: Optional[int] = Field(
|
|
485
|
+
default=None,
|
|
486
|
+
ge=1,
|
|
487
|
+
description="Interval in seconds for recurring jobs",
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
scheduled_time: Optional[str] = Field(
|
|
491
|
+
default=None,
|
|
492
|
+
description="ISO datetime for one-time scheduled job (e.g., '2025-12-31T23:59:59')",
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# Job configuration
|
|
496
|
+
queue: str = Field(
|
|
497
|
+
default="default",
|
|
498
|
+
description="Queue name to enqueue job",
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
timeout: Optional[int] = Field(
|
|
502
|
+
default=None,
|
|
503
|
+
ge=1,
|
|
504
|
+
description="Job timeout in seconds (overrides queue default)",
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
result_ttl: Optional[int] = Field(
|
|
508
|
+
default=None,
|
|
509
|
+
ge=0,
|
|
510
|
+
description="Result TTL in seconds (overrides queue default)",
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# Function arguments
|
|
514
|
+
args: List[Any] = Field(
|
|
515
|
+
default_factory=list,
|
|
516
|
+
description="Positional arguments for function",
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
kwargs: Dict[str, Any] = Field(
|
|
520
|
+
default_factory=dict,
|
|
521
|
+
description="Keyword arguments for function",
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Metadata
|
|
525
|
+
job_id: Optional[str] = Field(
|
|
526
|
+
default=None,
|
|
527
|
+
description="Custom job ID (generated if not provided)",
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
description: Optional[str] = Field(
|
|
531
|
+
default=None,
|
|
532
|
+
description="Human-readable description of the job",
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
repeat: Optional[int] = Field(
|
|
536
|
+
default=None,
|
|
537
|
+
ge=1,
|
|
538
|
+
description="Number of times to repeat (None = repeat forever for interval jobs)",
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Common task parameters (automatically added to kwargs)
|
|
542
|
+
limit: Optional[int] = Field(
|
|
543
|
+
default=None,
|
|
544
|
+
ge=1,
|
|
545
|
+
description="Limit parameter for task (automatically added to kwargs)",
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
verbosity: Optional[int] = Field(
|
|
549
|
+
default=None,
|
|
550
|
+
ge=0,
|
|
551
|
+
le=3,
|
|
552
|
+
description="Verbosity level 0-3 (automatically added to kwargs)",
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
report_type: Optional[str] = Field(
|
|
556
|
+
default=None,
|
|
557
|
+
description="Report type parameter (automatically added to kwargs)",
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
days: Optional[int] = Field(
|
|
561
|
+
default=None,
|
|
562
|
+
ge=1,
|
|
563
|
+
description="Days parameter for task (automatically added to kwargs)",
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
force: Optional[bool] = Field(
|
|
567
|
+
default=None,
|
|
568
|
+
description="Force parameter for task (automatically added to kwargs)",
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
@field_validator("cron", "interval", "scheduled_time")
|
|
572
|
+
@classmethod
|
|
573
|
+
def validate_schedule_type(cls, v, info):
|
|
574
|
+
"""Ensure at least one schedule type is provided."""
|
|
575
|
+
# This validator is called for each field, so we check after all fields are set
|
|
576
|
+
return v
|
|
577
|
+
|
|
578
|
+
@model_validator(mode="after")
|
|
579
|
+
def validate_one_schedule_type(self):
|
|
580
|
+
"""Ensure exactly one schedule type is provided and collect task parameters into kwargs."""
|
|
581
|
+
schedule_types = [
|
|
582
|
+
self.cron is not None,
|
|
583
|
+
self.interval is not None,
|
|
584
|
+
self.scheduled_time is not None,
|
|
585
|
+
]
|
|
586
|
+
|
|
587
|
+
if sum(schedule_types) == 0:
|
|
588
|
+
raise ValueError(
|
|
589
|
+
"At least one schedule type must be provided: cron, interval, or scheduled_time"
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
if sum(schedule_types) > 1:
|
|
593
|
+
raise ValueError(
|
|
594
|
+
"Only one schedule type can be provided: cron, interval, or scheduled_time"
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Collect task parameters into kwargs (declarative syntax support)
|
|
598
|
+
task_params = {}
|
|
599
|
+
|
|
600
|
+
# Common task parameters that should go into kwargs
|
|
601
|
+
param_fields = ['limit', 'verbosity', 'report_type', 'days', 'force']
|
|
602
|
+
|
|
603
|
+
for field_name in param_fields:
|
|
604
|
+
field_value = getattr(self, field_name, None)
|
|
605
|
+
if field_value is not None:
|
|
606
|
+
task_params[field_name] = field_value
|
|
607
|
+
|
|
608
|
+
if task_params:
|
|
609
|
+
# Merge task params with existing kwargs
|
|
610
|
+
# Use object.__setattr__ to avoid recursion with validate_assignment=True
|
|
611
|
+
merged_kwargs = {**self.kwargs, **task_params}
|
|
612
|
+
object.__setattr__(self, 'kwargs', merged_kwargs)
|
|
613
|
+
|
|
614
|
+
return self
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
__all__ = [
|
|
618
|
+
"RQQueueConfig",
|
|
619
|
+
"DjangoRQConfig",
|
|
620
|
+
"RQScheduleConfig",
|
|
621
|
+
]
|
|
@@ -106,7 +106,7 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
|
|
|
106
106
|
leads_enabled = base_module.is_leads_enabled()
|
|
107
107
|
knowbase_enabled = base_module.is_knowbase_enabled()
|
|
108
108
|
agents_enabled = base_module.is_agents_enabled()
|
|
109
|
-
tasks_enabled = base_module.
|
|
109
|
+
tasks_enabled = base_module.should_enable_rearq()
|
|
110
110
|
payments_enabled = base_module.is_payments_enabled()
|
|
111
111
|
|
|
112
112
|
# Add Support zone if enabled
|
django_cfg/modules/base.py
CHANGED
|
@@ -156,16 +156,14 @@ class BaseCfgModule(ABC):
|
|
|
156
156
|
"""
|
|
157
157
|
return self._get_config_key('enable_knowbase', False)
|
|
158
158
|
|
|
159
|
-
def
|
|
159
|
+
def should_enable_rq(self) -> bool:
|
|
160
160
|
"""
|
|
161
|
-
Check if django-cfg
|
|
162
|
-
Auto-enables if knowbase or agents are enabled.
|
|
161
|
+
Check if django-cfg RQ is enabled.
|
|
163
162
|
|
|
164
163
|
Returns:
|
|
165
|
-
True if
|
|
164
|
+
True if RQ is enabled, False otherwise
|
|
166
165
|
"""
|
|
167
|
-
|
|
168
|
-
return self.get_config().should_enable_tasks()
|
|
166
|
+
return self.get_config().should_enable_rq()
|
|
169
167
|
|
|
170
168
|
def is_maintenance_enabled(self) -> bool:
|
|
171
169
|
"""
|
|
@@ -18,7 +18,7 @@ class BackgroundTaskConfig(BaseModel):
|
|
|
18
18
|
```python
|
|
19
19
|
background_config = BackgroundTaskConfig(
|
|
20
20
|
enabled=True,
|
|
21
|
-
task_runner='
|
|
21
|
+
task_runner='django_rq',
|
|
22
22
|
batch_size=100,
|
|
23
23
|
timeout=300,
|
|
24
24
|
)
|
|
@@ -35,9 +35,9 @@ class BackgroundTaskConfig(BaseModel):
|
|
|
35
35
|
description="Enable background task processing"
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
-
task_runner: Literal['
|
|
39
|
-
'
|
|
40
|
-
description="Task runner to use for background operations"
|
|
38
|
+
task_runner: Literal['django_rq', 'sync'] = Field(
|
|
39
|
+
'sync',
|
|
40
|
+
description="Task runner to use for background operations (django_rq or sync)"
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
batch_size: int = Field(
|
|
@@ -60,7 +60,14 @@ class CompositionElements:
|
|
|
60
60
|
# Material Icon
|
|
61
61
|
icon_html = CompositionElements.icon(icon_str, size=icon_size)
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
# DON'T escape SafeString - it's already safe HTML!
|
|
64
|
+
from django.utils.safestring import SafeString
|
|
65
|
+
if isinstance(text, SafeString):
|
|
66
|
+
text_html = text
|
|
67
|
+
else:
|
|
68
|
+
text_html = escape(str(text))
|
|
69
|
+
|
|
70
|
+
return format_html('{}{}<span>{}</span>', icon_html, separator, text_html)
|
|
64
71
|
|
|
65
72
|
@staticmethod
|
|
66
73
|
def inline(*items, separator: str = " | ",
|
|
@@ -107,7 +114,7 @@ class CompositionElements:
|
|
|
107
114
|
else:
|
|
108
115
|
processed_items.append(escape(str(item)))
|
|
109
116
|
|
|
110
|
-
# Join with separator
|
|
117
|
+
# Join with separator - str() doesn't lose SafeString when joined then mark_safe'd
|
|
111
118
|
joined = mark_safe(separator.join(str(item) for item in processed_items))
|
|
112
119
|
|
|
113
120
|
return format_html('<span class="{}">{}</span>', classes, joined)
|