django-cfg 1.4.111__py3-none-any.whl → 1.4.114__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/serializers/__init__.py +12 -0
- django_cfg/apps/dashboard/serializers/django_q2.py +50 -0
- django_cfg/apps/dashboard/serializers/overview.py +22 -11
- django_cfg/apps/dashboard/services/__init__.py +2 -0
- django_cfg/apps/dashboard/services/django_q2_service.py +159 -0
- django_cfg/apps/dashboard/services/system_health_service.py +85 -0
- django_cfg/apps/dashboard/urls.py +2 -0
- django_cfg/apps/dashboard/views/__init__.py +2 -0
- django_cfg/apps/dashboard/views/django_q2_views.py +79 -0
- django_cfg/apps/dashboard/views/overview_views.py +16 -2
- django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +16 -0
- django_cfg/config.py +3 -4
- django_cfg/core/base/config_model.py +18 -1
- django_cfg/core/builders/apps_builder.py +4 -0
- django_cfg/core/generation/data_generators/cache.py +28 -2
- django_cfg/core/generation/integration_generators/__init__.py +4 -0
- django_cfg/core/generation/integration_generators/django_q2.py +133 -0
- django_cfg/core/generation/orchestrator.py +13 -0
- django_cfg/core/integration/display/startup.py +2 -2
- django_cfg/core/integration/url_integration.py +2 -2
- django_cfg/models/__init__.py +3 -0
- django_cfg/models/django/__init__.py +3 -0
- django_cfg/models/django/django_q2.py +491 -0
- django_cfg/modules/django_admin/utils/html_builder.py +50 -2
- django_cfg/pyproject.toml +2 -2
- django_cfg/registry/core.py +4 -0
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +389 -166
- django_cfg/templatetags/django_cfg.py +8 -0
- {django_cfg-1.4.111.dist-info → django_cfg-1.4.114.dist-info}/METADATA +3 -1
- {django_cfg-1.4.111.dist-info → django_cfg-1.4.114.dist-info}/RECORD +35 -29
- {django_cfg-1.4.111.dist-info → django_cfg-1.4.114.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.111.dist-info → django_cfg-1.4.114.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.111.dist-info → django_cfg-1.4.114.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django-Q2 Configuration for django-cfg.
|
|
3
|
+
|
|
4
|
+
Type-safe configuration for django-q2 (modern fork of django-q) with automatic
|
|
5
|
+
Django settings generation and support for scheduled tasks.
|
|
6
|
+
|
|
7
|
+
Django-Q2 is the actively maintained fork: https://github.com/django-q2/django-q2
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Type-safe scheduled task definitions
|
|
11
|
+
- Django management command support
|
|
12
|
+
- Cron-style and interval-based scheduling
|
|
13
|
+
- Redis/database broker configuration
|
|
14
|
+
- Queue management
|
|
15
|
+
- Task result storage
|
|
16
|
+
- Monitoring and logging
|
|
17
|
+
- Built-in admin interface
|
|
18
|
+
- Python 3.8+ and Django 3.2+ support
|
|
19
|
+
|
|
20
|
+
Migration from django-crontab to django-q2:
|
|
21
|
+
1. Replace django-crontab with django-q2 in requirements
|
|
22
|
+
2. Convert CrontabConfig to DjangoQ2Config
|
|
23
|
+
3. Cron expressions stay the same
|
|
24
|
+
4. Add additional features like intervals, hooks, retries
|
|
25
|
+
5. Built-in admin interface for monitoring
|
|
26
|
+
6. No need to run 'crontab add' - uses Django's own scheduler
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
```python
|
|
30
|
+
# Old django-crontab
|
|
31
|
+
crontab_config = CrontabConfig(
|
|
32
|
+
jobs=[
|
|
33
|
+
CrontabJobConfig(
|
|
34
|
+
name="Sync balances",
|
|
35
|
+
minute="0",
|
|
36
|
+
hour="*/1", # Every hour
|
|
37
|
+
command="sync_account_balances",
|
|
38
|
+
)
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# New django-q2
|
|
43
|
+
django_q2_config = DjangoQ2Config(
|
|
44
|
+
schedules=[
|
|
45
|
+
DjangoQ2ScheduleConfig(
|
|
46
|
+
name="Sync balances",
|
|
47
|
+
schedule_type="hourly", # Simpler!
|
|
48
|
+
command="sync_account_balances",
|
|
49
|
+
)
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
56
|
+
|
|
57
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DjangoQ2ScheduleConfig(BaseModel):
|
|
61
|
+
"""
|
|
62
|
+
Configuration for a single Django-Q2 scheduled task.
|
|
63
|
+
|
|
64
|
+
Supports both cron-style and interval-based scheduling.
|
|
65
|
+
|
|
66
|
+
Schedule types:
|
|
67
|
+
- cron: Traditional cron expression (e.g., "0 0 * * *")
|
|
68
|
+
- minutes: Every N minutes (e.g., minutes=15)
|
|
69
|
+
- hourly: Every hour (at minute 0)
|
|
70
|
+
- daily: Every day (at midnight)
|
|
71
|
+
- weekly: Every week (Sunday at midnight)
|
|
72
|
+
- monthly: Every month (1st at midnight)
|
|
73
|
+
- yearly: Every year (Jan 1st at midnight)
|
|
74
|
+
- once: Run once at next scheduled time
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
model_config = ConfigDict(
|
|
78
|
+
validate_assignment=True,
|
|
79
|
+
extra="forbid",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Task identification
|
|
83
|
+
name: str = Field(
|
|
84
|
+
...,
|
|
85
|
+
description="Human-readable task name (unique identifier)",
|
|
86
|
+
min_length=1,
|
|
87
|
+
max_length=100,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Schedule type
|
|
91
|
+
schedule_type: Literal["cron", "minutes", "hourly", "daily", "weekly", "monthly", "yearly", "once"] = Field(
|
|
92
|
+
default="cron",
|
|
93
|
+
description="Schedule type: cron, minutes, hourly, daily, weekly, monthly, yearly, once",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Cron-style schedule (when schedule_type="cron")
|
|
97
|
+
cron: Optional[str] = Field(
|
|
98
|
+
default=None,
|
|
99
|
+
description="Cron expression (e.g., '0 0 * * *' for daily at midnight)",
|
|
100
|
+
pattern=r"^[\d\*\-\,\/\s]+$",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Interval-based schedule (when schedule_type="minutes")
|
|
104
|
+
minutes: Optional[int] = Field(
|
|
105
|
+
default=None,
|
|
106
|
+
ge=1,
|
|
107
|
+
le=525600, # Max 1 year in minutes
|
|
108
|
+
description="Run every N minutes (when schedule_type='minutes')",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Task execution configuration
|
|
112
|
+
func: Optional[str] = Field(
|
|
113
|
+
default=None,
|
|
114
|
+
description="Function path (e.g., 'django.core.management.call_command' or 'myapp.tasks.my_task'). Auto-set when 'command' is provided.",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Function arguments
|
|
118
|
+
args: Optional[List[Any]] = Field(
|
|
119
|
+
default=None,
|
|
120
|
+
description="Positional arguments for the function",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
kwargs: Optional[Dict[str, Any]] = Field(
|
|
124
|
+
default=None,
|
|
125
|
+
description="Keyword arguments for the function",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Management command support (shortcut)
|
|
129
|
+
command: Optional[str] = Field(
|
|
130
|
+
default=None,
|
|
131
|
+
description="Django management command name (auto-sets func to call_command)",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
command_args: Optional[List[str]] = Field(
|
|
135
|
+
default=None,
|
|
136
|
+
description="Management command arguments",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
command_kwargs: Optional[Dict[str, Any]] = Field(
|
|
140
|
+
default=None,
|
|
141
|
+
description="Management command keyword arguments",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Task options
|
|
145
|
+
enabled: bool = Field(
|
|
146
|
+
default=True,
|
|
147
|
+
description="Whether task is enabled",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
queue: Optional[str] = Field(
|
|
151
|
+
default=None,
|
|
152
|
+
description="Queue name (None = default queue)",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
timeout: Optional[int] = Field(
|
|
156
|
+
default=None,
|
|
157
|
+
ge=1,
|
|
158
|
+
le=86400, # Max 24 hours
|
|
159
|
+
description="Task timeout in seconds",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
repeats: int = Field(
|
|
163
|
+
default=-1,
|
|
164
|
+
description="Number of times to repeat (-1 = infinite)",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
hook: Optional[str] = Field(
|
|
168
|
+
default=None,
|
|
169
|
+
description="Hook function to call after task completion",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
cluster: Optional[str] = Field(
|
|
173
|
+
default=None,
|
|
174
|
+
description="Cluster name (for multi-cluster setups)",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def model_post_init(self, __context: Any) -> None:
|
|
178
|
+
"""Validate and setup configuration after initialization."""
|
|
179
|
+
# Validate that either func or command is provided
|
|
180
|
+
if not self.func and not self.command:
|
|
181
|
+
raise ValueError("Either 'func' or 'command' must be provided")
|
|
182
|
+
|
|
183
|
+
# Setup management command
|
|
184
|
+
if self.command:
|
|
185
|
+
self.func = "django.core.management.call_command"
|
|
186
|
+
args = [self.command]
|
|
187
|
+
if self.command_args:
|
|
188
|
+
args.extend(self.command_args)
|
|
189
|
+
self.args = args
|
|
190
|
+
if self.command_kwargs:
|
|
191
|
+
self.kwargs = self.command_kwargs
|
|
192
|
+
|
|
193
|
+
# Validate schedule configuration
|
|
194
|
+
if self.schedule_type == "cron" and not self.cron:
|
|
195
|
+
raise ValueError("'cron' must be set when schedule_type is 'cron'")
|
|
196
|
+
|
|
197
|
+
if self.schedule_type == "minutes" and not self.minutes:
|
|
198
|
+
raise ValueError("'minutes' must be set when schedule_type is 'minutes'")
|
|
199
|
+
|
|
200
|
+
def to_django_q_format(self) -> Dict[str, Any]:
|
|
201
|
+
"""
|
|
202
|
+
Convert to Django-Q Schedule format.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Dictionary for ORM.create() or schedule creation
|
|
206
|
+
"""
|
|
207
|
+
# Map our schedule types to Django-Q2 constants
|
|
208
|
+
type_mapping = {
|
|
209
|
+
"once": "O",
|
|
210
|
+
"minutes": "I",
|
|
211
|
+
"hourly": "H",
|
|
212
|
+
"daily": "D",
|
|
213
|
+
"weekly": "W",
|
|
214
|
+
"monthly": "M",
|
|
215
|
+
"yearly": "Y",
|
|
216
|
+
"cron": "C",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
config = {
|
|
220
|
+
"name": self.name,
|
|
221
|
+
"func": self.func,
|
|
222
|
+
"schedule_type": type_mapping.get(self.schedule_type, self.schedule_type.upper()),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if self.args:
|
|
226
|
+
config["args"] = str(self.args)
|
|
227
|
+
|
|
228
|
+
if self.kwargs:
|
|
229
|
+
config["kwargs"] = str(self.kwargs)
|
|
230
|
+
|
|
231
|
+
if self.schedule_type == "cron" and self.cron:
|
|
232
|
+
config["cron"] = self.cron
|
|
233
|
+
|
|
234
|
+
if self.schedule_type == "minutes" and self.minutes:
|
|
235
|
+
config["minutes"] = self.minutes
|
|
236
|
+
|
|
237
|
+
if self.queue:
|
|
238
|
+
config["queue"] = self.queue
|
|
239
|
+
|
|
240
|
+
if self.timeout:
|
|
241
|
+
config["timeout"] = self.timeout
|
|
242
|
+
|
|
243
|
+
if self.repeats != -1:
|
|
244
|
+
config["repeats"] = self.repeats
|
|
245
|
+
|
|
246
|
+
if self.hook:
|
|
247
|
+
config["hook"] = self.hook
|
|
248
|
+
|
|
249
|
+
if self.cluster:
|
|
250
|
+
config["cluster"] = self.cluster
|
|
251
|
+
|
|
252
|
+
return config
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class DjangoQ2Config(BaseModel):
|
|
256
|
+
"""
|
|
257
|
+
Complete Django-Q2 configuration container.
|
|
258
|
+
|
|
259
|
+
Integrates with django-q2 (modern fork) for scheduled and async task execution.
|
|
260
|
+
Automatically adds django_q to INSTALLED_APPS when enabled.
|
|
261
|
+
|
|
262
|
+
Installation:
|
|
263
|
+
pip install django-q2[redis]
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
```python
|
|
267
|
+
# MAGIC: broker_url automatically uses config.redis_url! 🎉
|
|
268
|
+
# Just set redis_url once in your DjangoConfig:
|
|
269
|
+
# redis_url: Optional[str] = env.redis_url
|
|
270
|
+
|
|
271
|
+
django_q2_config = DjangoQ2Config(
|
|
272
|
+
enabled=True,
|
|
273
|
+
# broker_url is auto-detected from config.redis_url!
|
|
274
|
+
schedules=[
|
|
275
|
+
DjangoQ2ScheduleConfig(
|
|
276
|
+
name="Sync balances every hour",
|
|
277
|
+
schedule_type="hourly",
|
|
278
|
+
command="sync_account_balances",
|
|
279
|
+
),
|
|
280
|
+
DjangoQ2ScheduleConfig(
|
|
281
|
+
name="Cleanup old data daily",
|
|
282
|
+
schedule_type="cron",
|
|
283
|
+
cron="0 2 * * *", # 2 AM daily
|
|
284
|
+
command="cleanup_old_data",
|
|
285
|
+
command_kwargs={"days": 30},
|
|
286
|
+
),
|
|
287
|
+
DjangoQ2ScheduleConfig(
|
|
288
|
+
name="Quick check every 5 minutes",
|
|
289
|
+
schedule_type="minutes",
|
|
290
|
+
minutes=5,
|
|
291
|
+
command="health_check",
|
|
292
|
+
),
|
|
293
|
+
],
|
|
294
|
+
)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Admin interface:
|
|
298
|
+
- Visit /admin/django_q/ to view tasks and schedules
|
|
299
|
+
- Monitor task execution, failures, and performance
|
|
300
|
+
- Manually trigger scheduled tasks
|
|
301
|
+
- View task results and logs
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
model_config = ConfigDict(
|
|
305
|
+
validate_assignment=True,
|
|
306
|
+
extra="forbid",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
enabled: bool = Field(
|
|
310
|
+
default=True,
|
|
311
|
+
description="Enable Django-Q (auto-adds django_q to INSTALLED_APPS)",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
schedules: List[DjangoQ2ScheduleConfig] = Field(
|
|
315
|
+
default_factory=list,
|
|
316
|
+
description="List of scheduled tasks",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Django-Q broker configuration
|
|
320
|
+
broker_url: str = Field(
|
|
321
|
+
default="redis://localhost:6379/0",
|
|
322
|
+
description="Broker URL (Redis recommended for production)",
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
broker_class: Literal["redis", "orm"] = Field(
|
|
326
|
+
default="redis",
|
|
327
|
+
description="Broker backend class (redis or orm)",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Queue configuration
|
|
331
|
+
queue_limit: Optional[int] = Field(
|
|
332
|
+
default=50,
|
|
333
|
+
ge=1,
|
|
334
|
+
description="Maximum tasks in queue before rejecting new ones",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
workers: int = Field(
|
|
338
|
+
default=4,
|
|
339
|
+
ge=1,
|
|
340
|
+
le=32,
|
|
341
|
+
description="Number of worker processes",
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
timeout: int = Field(
|
|
345
|
+
default=300,
|
|
346
|
+
ge=1,
|
|
347
|
+
le=86400, # Max 24 hours
|
|
348
|
+
description="Default task timeout in seconds",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
retry: int = Field(
|
|
352
|
+
default=3600,
|
|
353
|
+
ge=0,
|
|
354
|
+
description="Seconds to wait before retrying failed tasks (0 = no retry)",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Task result configuration
|
|
358
|
+
save_limit: int = Field(
|
|
359
|
+
default=250,
|
|
360
|
+
ge=0,
|
|
361
|
+
description="Maximum number of successful tasks to save (0 = unlimited)",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
cached: int = Field(
|
|
365
|
+
default=500,
|
|
366
|
+
ge=0,
|
|
367
|
+
description="Maximum number of tasks to cache (0 = disabled)",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Monitoring
|
|
371
|
+
monitor_interval: int = Field(
|
|
372
|
+
default=30,
|
|
373
|
+
ge=1,
|
|
374
|
+
description="Seconds between monitor checks",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Logging
|
|
378
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(
|
|
379
|
+
default="INFO",
|
|
380
|
+
description="Django-Q log level",
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Advanced options
|
|
384
|
+
compress: bool = Field(
|
|
385
|
+
default=False,
|
|
386
|
+
description="Compress task data",
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
catch_up: bool = Field(
|
|
390
|
+
default=True,
|
|
391
|
+
description="Run missed scheduled tasks immediately",
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
sync: bool = Field(
|
|
395
|
+
default=False,
|
|
396
|
+
description="Run tasks synchronously (for testing)",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
def get_enabled_schedules(self) -> List[DjangoQ2ScheduleConfig]:
|
|
400
|
+
"""Get list of enabled schedules."""
|
|
401
|
+
return [schedule for schedule in self.schedules if schedule.enabled]
|
|
402
|
+
|
|
403
|
+
def to_django_settings(self, parent_config: Optional[Any] = None) -> Dict[str, Any]:
|
|
404
|
+
"""
|
|
405
|
+
Convert to Django settings dictionary.
|
|
406
|
+
|
|
407
|
+
Generates Q_CLUSTER configuration for Django-Q2.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
parent_config: Optional parent DjangoConfig for accessing redis_url
|
|
411
|
+
|
|
412
|
+
Note: Schedules are created via Django ORM, not settings.
|
|
413
|
+
Use management command: python manage.py qcluster
|
|
414
|
+
"""
|
|
415
|
+
if not self.enabled:
|
|
416
|
+
return {}
|
|
417
|
+
|
|
418
|
+
# Auto-detect redis_url from parent config if not explicitly set
|
|
419
|
+
broker_url = self.broker_url
|
|
420
|
+
if broker_url == "redis://localhost:6379/0" and parent_config:
|
|
421
|
+
# Use redis_url from parent config if available
|
|
422
|
+
if hasattr(parent_config, 'redis_url') and parent_config.redis_url:
|
|
423
|
+
broker_url = parent_config.redis_url
|
|
424
|
+
|
|
425
|
+
# Map short broker names to full class paths
|
|
426
|
+
broker_class_map = {
|
|
427
|
+
"redis": "django_q.brokers.redis_broker.Redis",
|
|
428
|
+
"orm": "django_q.brokers.orm.ORM",
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
cluster_config = {
|
|
432
|
+
# Broker
|
|
433
|
+
"name": "django_cfg_cluster",
|
|
434
|
+
"broker": broker_url,
|
|
435
|
+
"broker_class": broker_class_map.get(self.broker_class, self.broker_class),
|
|
436
|
+
|
|
437
|
+
# Queue
|
|
438
|
+
"queue_limit": self.queue_limit,
|
|
439
|
+
"workers": self.workers,
|
|
440
|
+
"timeout": self.timeout,
|
|
441
|
+
"retry": self.retry,
|
|
442
|
+
|
|
443
|
+
# Results
|
|
444
|
+
"save_limit": self.save_limit,
|
|
445
|
+
"cached": self.cached,
|
|
446
|
+
|
|
447
|
+
# Monitoring
|
|
448
|
+
"monitor": self.monitor_interval,
|
|
449
|
+
|
|
450
|
+
# Logging
|
|
451
|
+
"log_level": self.log_level,
|
|
452
|
+
|
|
453
|
+
# Advanced
|
|
454
|
+
"compress": self.compress,
|
|
455
|
+
"catch_up": self.catch_up,
|
|
456
|
+
"sync": self.sync,
|
|
457
|
+
|
|
458
|
+
# Django integration
|
|
459
|
+
"orm": "default",
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
# Only add django_redis if broker is actually Redis
|
|
463
|
+
if self.broker_class == "redis":
|
|
464
|
+
# Don't set django_redis - let Django-Q2 connect directly via broker URL
|
|
465
|
+
pass
|
|
466
|
+
|
|
467
|
+
settings = {
|
|
468
|
+
"Q_CLUSTER": cluster_config
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return settings
|
|
472
|
+
|
|
473
|
+
def get_schedule_by_name(self, name: str) -> Optional[DjangoQ2ScheduleConfig]:
|
|
474
|
+
"""Get schedule by name."""
|
|
475
|
+
for schedule in self.schedules:
|
|
476
|
+
if schedule.name == name:
|
|
477
|
+
return schedule
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
def get_schedules_by_command(self, command: str) -> List[DjangoQ2ScheduleConfig]:
|
|
481
|
+
"""Get all schedules for a specific command."""
|
|
482
|
+
return [
|
|
483
|
+
schedule for schedule in self.schedules
|
|
484
|
+
if schedule.command == command
|
|
485
|
+
]
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
__all__ = [
|
|
489
|
+
"DjangoQ2ScheduleConfig",
|
|
490
|
+
"DjangoQ2Config",
|
|
491
|
+
]
|
|
@@ -4,6 +4,7 @@ Universal HTML builder for Django Admin display methods.
|
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, List, Optional, Union
|
|
7
|
+
import re
|
|
7
8
|
|
|
8
9
|
from django.utils.html import escape, format_html
|
|
9
10
|
from django.utils.safestring import SafeString
|
|
@@ -119,8 +120,17 @@ class HtmlBuilder:
|
|
|
119
120
|
if css_class:
|
|
120
121
|
classes += f" {css_class}"
|
|
121
122
|
|
|
122
|
-
#
|
|
123
|
-
|
|
123
|
+
# Convert items to strings, keeping SafeString as-is
|
|
124
|
+
from django.utils.safestring import SafeString, mark_safe
|
|
125
|
+
processed_items = []
|
|
126
|
+
for item in items:
|
|
127
|
+
if isinstance(item, (SafeString, str)):
|
|
128
|
+
processed_items.append(item)
|
|
129
|
+
else:
|
|
130
|
+
processed_items.append(escape(str(item)))
|
|
131
|
+
|
|
132
|
+
# Join with separator
|
|
133
|
+
joined = mark_safe(separator.join(str(item) for item in processed_items))
|
|
124
134
|
|
|
125
135
|
return format_html('<span class="{}">{}</span>', classes, joined)
|
|
126
136
|
|
|
@@ -320,6 +330,44 @@ class HtmlBuilder:
|
|
|
320
330
|
enable_plugins=enable_plugins
|
|
321
331
|
)
|
|
322
332
|
|
|
333
|
+
@staticmethod
|
|
334
|
+
def uuid_short(uuid_value: Any, length: int = 6, show_tooltip: bool = True) -> SafeString:
|
|
335
|
+
"""
|
|
336
|
+
Shorten UUID to first N characters with optional tooltip.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
uuid_value: UUID string or UUID object
|
|
340
|
+
length: Number of characters to show (default: 6)
|
|
341
|
+
show_tooltip: Show full UUID on hover (default: True)
|
|
342
|
+
|
|
343
|
+
Usage:
|
|
344
|
+
html.uuid_short(obj.id) # "a1b2c3..."
|
|
345
|
+
html.uuid_short(obj.id, length=8) # "a1b2c3d4..."
|
|
346
|
+
html.uuid_short(obj.id, show_tooltip=False) # Just short version
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
SafeString with shortened UUID
|
|
350
|
+
"""
|
|
351
|
+
uuid_str = str(uuid_value)
|
|
352
|
+
|
|
353
|
+
# Remove dashes for cleaner display
|
|
354
|
+
uuid_clean = uuid_str.replace('-', '')
|
|
355
|
+
|
|
356
|
+
# Take first N characters
|
|
357
|
+
short_uuid = uuid_clean[:length]
|
|
358
|
+
|
|
359
|
+
if show_tooltip:
|
|
360
|
+
return format_html(
|
|
361
|
+
'<code class="font-mono text-xs bg-base-100 dark:bg-base-800 px-1.5 py-0.5 rounded cursor-help" title="{}">{}</code>',
|
|
362
|
+
uuid_str,
|
|
363
|
+
short_uuid
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return format_html(
|
|
367
|
+
'<code class="font-mono text-xs bg-base-100 dark:bg-base-800 px-1.5 py-0.5 rounded">{}</code>',
|
|
368
|
+
short_uuid
|
|
369
|
+
)
|
|
370
|
+
|
|
323
371
|
@staticmethod
|
|
324
372
|
def markdown_docs(
|
|
325
373
|
content: Union[str, Path],
|
django_cfg/pyproject.toml
CHANGED
|
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "django-cfg"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.114"
|
|
8
8
|
description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "nextjs-admin", "react-admin", "websocket", "centrifugo", "real-time", "typescript-generation", "ai-agents", "enterprise-django", "django-settings", "type-safe-config", "modern-django",]
|
|
11
11
|
classifiers = [ "Development Status :: 4 - Beta", "Framework :: Django", "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Systems Administration", "Typing :: Typed",]
|
|
12
12
|
requires-python = ">=3.12,<3.14"
|
|
13
|
-
dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "rearq>=0.2.0,<1.0", "setuptools>=75.0.0; python_version>='3.13'", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)", "jinja2 (>=3.1.6,<4.0.0)", "django-axes[ipware] (>=8.0.0,<9.0.0)", "pydantic-settings (>=2.11.0,<3.0.0)", "pytz>=2025.1", "httpx>=0.28.1,<1.0", "mistune>=3.1.4,<4.0",]
|
|
13
|
+
dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "django-q2==1.8.0", "croniter>=6.0.0,<7.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "rearq>=0.2.0,<1.0", "setuptools>=75.0.0; python_version>='3.13'", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)", "jinja2 (>=3.1.6,<4.0.0)", "django-axes[ipware] (>=8.0.0,<9.0.0)", "pydantic-settings (>=2.11.0,<3.0.0)", "pytz>=2025.1", "httpx>=0.28.1,<1.0", "mistune>=3.1.4,<4.0",]
|
|
14
14
|
[[project.authors]]
|
|
15
15
|
name = "Django-CFG Team"
|
|
16
16
|
email = "info@djangocfg.com"
|
django_cfg/registry/core.py
CHANGED
|
@@ -38,6 +38,10 @@ CORE_REGISTRY = {
|
|
|
38
38
|
# Security - Django Crypto Fields
|
|
39
39
|
"CryptoFieldsConfig": ("django_cfg.models.django.crypto_fields", "CryptoFieldsConfig"),
|
|
40
40
|
|
|
41
|
+
# Scheduling - Django-Q2
|
|
42
|
+
"DjangoQ2Config": ("django_cfg.models.django.django_q2", "DjangoQ2Config"),
|
|
43
|
+
"DjangoQ2ScheduleConfig": ("django_cfg.models.django.django_q2", "DjangoQ2ScheduleConfig"),
|
|
44
|
+
|
|
41
45
|
# Limits models
|
|
42
46
|
"LimitsConfig": ("django_cfg.models.api.limits", "LimitsConfig"),
|
|
43
47
|
|
|
Binary file
|