django-cfg 1.4.113__py3-none-any.whl → 1.4.116__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 +10 -8
- django_cfg/apps/dashboard/serializers/django_q2.py +50 -0
- django_cfg/apps/dashboard/services/__init__.py +2 -2
- django_cfg/apps/dashboard/services/django_q2_service.py +159 -0
- django_cfg/apps/dashboard/services/system_health_service.py +39 -26
- django_cfg/apps/dashboard/urls.py +2 -2
- django_cfg/apps/dashboard/views/__init__.py +2 -2
- django_cfg/apps/dashboard/views/django_q2_views.py +79 -0
- django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +16 -0
- django_cfg/core/base/config_model.py +15 -5
- django_cfg/core/builders/apps_builder.py +3 -3
- django_cfg/core/generation/data_generators/cache.py +28 -2
- django_cfg/core/generation/integration_generators/__init__.py +4 -3
- django_cfg/core/generation/integration_generators/django_q2.py +133 -0
- django_cfg/core/generation/orchestrator.py +7 -7
- django_cfg/models/__init__.py +3 -3
- django_cfg/models/django/__init__.py +3 -3
- django_cfg/models/django/django_q2.py +496 -0
- django_cfg/pyproject.toml +2 -2
- django_cfg/registry/core.py +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.4.113.dist-info → django_cfg-1.4.116.dist-info}/METADATA +3 -2
- {django_cfg-1.4.113.dist-info → django_cfg-1.4.116.dist-info}/RECORD +27 -26
- django_cfg/apps/dashboard/serializers/crontab.py +0 -84
- django_cfg/apps/dashboard/services/crontab_service.py +0 -210
- django_cfg/apps/dashboard/views/crontab_views.py +0 -72
- django_cfg/core/generation/integration_generators/crontab.py +0 -64
- django_cfg/models/django/crontab.py +0 -303
- {django_cfg-1.4.113.dist-info → django_cfg-1.4.116.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.113.dist-info → django_cfg-1.4.116.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.113.dist-info → django_cfg-1.4.116.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django-Q2 settings generator for django-cfg.
|
|
3
|
+
|
|
4
|
+
Generates django-q2 settings and handles INSTALLED_APPS integration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from django_cfg.modules.django_logging import logger
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from django_cfg.models.django.django_q import DjangoQ2Config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DjangoQ2SettingsGenerator:
|
|
16
|
+
"""
|
|
17
|
+
Generates task scheduling settings for django-q2.
|
|
18
|
+
|
|
19
|
+
Automatically:
|
|
20
|
+
- Generates Q_CLUSTER configuration
|
|
21
|
+
- Adds django_q to INSTALLED_APPS if enabled
|
|
22
|
+
- Configures broker, workers, and task settings
|
|
23
|
+
- Provides schedule management via Django ORM
|
|
24
|
+
|
|
25
|
+
Django-Q2 vs django-crontab:
|
|
26
|
+
- No need to run 'crontab add' - schedules in database
|
|
27
|
+
- Built-in admin interface for monitoring
|
|
28
|
+
- Support for both cron and interval scheduling
|
|
29
|
+
- Task retries and hooks
|
|
30
|
+
- Async task support
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, config: "DjangoQ2Config", parent_config: Optional[Any] = None):
|
|
34
|
+
"""
|
|
35
|
+
Initialize with Django-Q2 configuration.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: DjangoQ2Config instance
|
|
39
|
+
parent_config: Optional parent DjangoConfig for accessing redis_url
|
|
40
|
+
"""
|
|
41
|
+
self.config = config
|
|
42
|
+
self.parent_config = parent_config
|
|
43
|
+
|
|
44
|
+
def generate(self) -> Dict[str, Any]:
|
|
45
|
+
"""
|
|
46
|
+
Generate Django-Q2 settings.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dictionary with Q_CLUSTER configuration
|
|
50
|
+
"""
|
|
51
|
+
if not self.config or not self.config.enabled:
|
|
52
|
+
return {}
|
|
53
|
+
|
|
54
|
+
settings = self.config.to_django_settings(parent_config=self.parent_config)
|
|
55
|
+
|
|
56
|
+
# Log configuration
|
|
57
|
+
logger.info(
|
|
58
|
+
f"✓ Configured Django-Q2 task queue "
|
|
59
|
+
f"[broker: {self.config.broker_class}, workers: {self.config.workers}]"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
enabled_schedules = self.config.get_enabled_schedules()
|
|
63
|
+
if enabled_schedules:
|
|
64
|
+
logger.info(
|
|
65
|
+
f"✓ Found {len(enabled_schedules)} scheduled task(s) "
|
|
66
|
+
f"[use admin or management commands to create schedules]"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Log individual schedules in debug mode
|
|
70
|
+
for schedule in enabled_schedules:
|
|
71
|
+
logger.debug(
|
|
72
|
+
f" - {schedule.name}: {schedule.schedule_type} → "
|
|
73
|
+
f"{schedule.command if schedule.command else schedule.func}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
logger.info(
|
|
77
|
+
"📝 To create schedules: python manage.py qcluster "
|
|
78
|
+
"or use Django admin at /admin/django_q/schedule/"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return settings
|
|
82
|
+
|
|
83
|
+
def generate_schedule_creation_code(self) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Generate Python code to create schedules programmatically.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Python code string for creating schedules
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
```python
|
|
92
|
+
code = generator.generate_schedule_creation_code()
|
|
93
|
+
# Use in management command or startup script
|
|
94
|
+
```
|
|
95
|
+
"""
|
|
96
|
+
if not self.config or not self.config.enabled:
|
|
97
|
+
return ""
|
|
98
|
+
|
|
99
|
+
enabled_schedules = self.config.get_enabled_schedules()
|
|
100
|
+
if not enabled_schedules:
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
lines = [
|
|
104
|
+
"# Auto-generated Django-Q2 schedule creation",
|
|
105
|
+
"from django_q.models import Schedule",
|
|
106
|
+
"",
|
|
107
|
+
"# Create or update schedules",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
for schedule in enabled_schedules:
|
|
111
|
+
schedule_dict = schedule.to_django_q_format()
|
|
112
|
+
|
|
113
|
+
lines.append("")
|
|
114
|
+
lines.append(f"# {schedule.name}")
|
|
115
|
+
lines.append("Schedule.objects.update_or_create(")
|
|
116
|
+
lines.append(f" name='{schedule.name}',")
|
|
117
|
+
lines.append(" defaults={")
|
|
118
|
+
|
|
119
|
+
for key, value in schedule_dict.items():
|
|
120
|
+
if key == "name":
|
|
121
|
+
continue
|
|
122
|
+
if isinstance(value, str):
|
|
123
|
+
lines.append(f" '{key}': '{value}',")
|
|
124
|
+
else:
|
|
125
|
+
lines.append(f" '{key}': {value},")
|
|
126
|
+
|
|
127
|
+
lines.append(" }")
|
|
128
|
+
lines.append(")")
|
|
129
|
+
|
|
130
|
+
return "\n".join(lines)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ = ["DjangoQ2SettingsGenerator"]
|
|
@@ -78,7 +78,7 @@ class SettingsOrchestrator:
|
|
|
78
78
|
settings.update(self._generate_third_party_settings())
|
|
79
79
|
settings.update(self._generate_api_settings())
|
|
80
80
|
settings.update(self._generate_tasks_settings())
|
|
81
|
-
settings.update(self.
|
|
81
|
+
settings.update(self._generate_django_q2_settings())
|
|
82
82
|
settings.update(self._generate_tailwind_settings())
|
|
83
83
|
|
|
84
84
|
# Apply additional settings (user overrides)
|
|
@@ -224,17 +224,17 @@ class SettingsOrchestrator:
|
|
|
224
224
|
except Exception as e:
|
|
225
225
|
raise ConfigurationError(f"Failed to generate tasks settings: {e}") from e
|
|
226
226
|
|
|
227
|
-
def
|
|
228
|
-
"""Generate
|
|
229
|
-
if not hasattr(self.config, "
|
|
227
|
+
def _generate_django_q2_settings(self) -> Dict[str, Any]:
|
|
228
|
+
"""Generate Django-Q2 task scheduling settings."""
|
|
229
|
+
if not hasattr(self.config, "django_q2") or not self.config.django_q2:
|
|
230
230
|
return {}
|
|
231
231
|
|
|
232
232
|
try:
|
|
233
|
-
from .integration_generators.
|
|
234
|
-
generator =
|
|
233
|
+
from .integration_generators.django_q2 import DjangoQ2SettingsGenerator
|
|
234
|
+
generator = DjangoQ2SettingsGenerator(self.config.django_q2, parent_config=self.config)
|
|
235
235
|
return generator.generate()
|
|
236
236
|
except Exception as e:
|
|
237
|
-
raise ConfigurationError(f"Failed to generate
|
|
237
|
+
raise ConfigurationError(f"Failed to generate Django-Q2 settings: {e}") from e
|
|
238
238
|
|
|
239
239
|
def _generate_tailwind_settings(self) -> Dict[str, Any]:
|
|
240
240
|
"""Generate Tailwind CSS settings."""
|
django_cfg/models/__init__.py
CHANGED
|
@@ -34,8 +34,8 @@ from .base.module import BaseCfgAutoModule
|
|
|
34
34
|
# Django-specific
|
|
35
35
|
from .django.axes import AxesConfig
|
|
36
36
|
from .django.constance import ConstanceConfig, ConstanceField
|
|
37
|
-
from .django.crontab import CrontabConfig, CrontabJobConfig
|
|
38
37
|
from .django.crypto_fields import CryptoFieldsConfig
|
|
38
|
+
from .django.django_q2 import DjangoQ2Config, DjangoQ2ScheduleConfig
|
|
39
39
|
from .django.environment import EnvironmentConfig
|
|
40
40
|
from .django.openapi import OpenAPIClientConfig
|
|
41
41
|
from .infrastructure.cache import CacheConfig
|
|
@@ -80,8 +80,8 @@ __all__ = [
|
|
|
80
80
|
"EnvironmentConfig",
|
|
81
81
|
"ConstanceConfig",
|
|
82
82
|
"ConstanceField",
|
|
83
|
-
"
|
|
84
|
-
"
|
|
83
|
+
"DjangoQ2Config",
|
|
84
|
+
"DjangoQ2ScheduleConfig",
|
|
85
85
|
"OpenAPIClientConfig",
|
|
86
86
|
"UnfoldConfig",
|
|
87
87
|
"AxesConfig",
|
|
@@ -6,7 +6,7 @@ Django integrations and extensions.
|
|
|
6
6
|
|
|
7
7
|
from .axes import AxesConfig
|
|
8
8
|
from .constance import ConstanceConfig, ConstanceField
|
|
9
|
-
from .
|
|
9
|
+
from .django_q2 import DjangoQ2Config, DjangoQ2ScheduleConfig
|
|
10
10
|
from .environment import EnvironmentConfig
|
|
11
11
|
from .openapi import OpenAPIClientConfig
|
|
12
12
|
|
|
@@ -14,8 +14,8 @@ __all__ = [
|
|
|
14
14
|
"EnvironmentConfig",
|
|
15
15
|
"ConstanceConfig",
|
|
16
16
|
"ConstanceField",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"DjangoQ2Config",
|
|
18
|
+
"DjangoQ2ScheduleConfig",
|
|
19
19
|
"OpenAPIClientConfig",
|
|
20
20
|
"AxesConfig",
|
|
21
21
|
]
|
|
@@ -0,0 +1,496 @@
|
|
|
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
|
+
from django.utils import timezone
|
|
208
|
+
from datetime import timedelta
|
|
209
|
+
|
|
210
|
+
# Map our schedule types to Django-Q2 constants
|
|
211
|
+
type_mapping = {
|
|
212
|
+
"once": "O",
|
|
213
|
+
"minutes": "I",
|
|
214
|
+
"hourly": "H",
|
|
215
|
+
"daily": "D",
|
|
216
|
+
"weekly": "W",
|
|
217
|
+
"monthly": "M",
|
|
218
|
+
"yearly": "Y",
|
|
219
|
+
"cron": "C",
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
config = {
|
|
223
|
+
"name": self.name,
|
|
224
|
+
"func": self.func,
|
|
225
|
+
"schedule_type": type_mapping.get(self.schedule_type, self.schedule_type.upper()),
|
|
226
|
+
# Set next_run to NOW + 10 seconds for immediate execution on qcluster start
|
|
227
|
+
"next_run": timezone.now() + timedelta(seconds=10),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if self.args:
|
|
231
|
+
config["args"] = self.args
|
|
232
|
+
|
|
233
|
+
if self.kwargs:
|
|
234
|
+
config["kwargs"] = self.kwargs
|
|
235
|
+
|
|
236
|
+
if self.schedule_type == "cron" and self.cron:
|
|
237
|
+
config["cron"] = self.cron
|
|
238
|
+
|
|
239
|
+
if self.schedule_type == "minutes" and self.minutes:
|
|
240
|
+
config["minutes"] = self.minutes
|
|
241
|
+
|
|
242
|
+
if self.queue:
|
|
243
|
+
config["queue"] = self.queue
|
|
244
|
+
|
|
245
|
+
if self.timeout:
|
|
246
|
+
config["timeout"] = self.timeout
|
|
247
|
+
|
|
248
|
+
if self.repeats != -1:
|
|
249
|
+
config["repeats"] = self.repeats
|
|
250
|
+
|
|
251
|
+
if self.hook:
|
|
252
|
+
config["hook"] = self.hook
|
|
253
|
+
|
|
254
|
+
if self.cluster:
|
|
255
|
+
config["cluster"] = self.cluster
|
|
256
|
+
|
|
257
|
+
return config
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class DjangoQ2Config(BaseModel):
|
|
261
|
+
"""
|
|
262
|
+
Complete Django-Q2 configuration container.
|
|
263
|
+
|
|
264
|
+
Integrates with django-q2 (modern fork) for scheduled and async task execution.
|
|
265
|
+
Automatically adds django_q to INSTALLED_APPS when enabled.
|
|
266
|
+
|
|
267
|
+
Installation:
|
|
268
|
+
pip install django-q2[redis]
|
|
269
|
+
|
|
270
|
+
Example:
|
|
271
|
+
```python
|
|
272
|
+
# MAGIC: broker_url automatically uses config.redis_url! 🎉
|
|
273
|
+
# Just set redis_url once in your DjangoConfig:
|
|
274
|
+
# redis_url: Optional[str] = env.redis_url
|
|
275
|
+
|
|
276
|
+
django_q2_config = DjangoQ2Config(
|
|
277
|
+
enabled=True,
|
|
278
|
+
# broker_url is auto-detected from config.redis_url!
|
|
279
|
+
schedules=[
|
|
280
|
+
DjangoQ2ScheduleConfig(
|
|
281
|
+
name="Sync balances every hour",
|
|
282
|
+
schedule_type="hourly",
|
|
283
|
+
command="sync_account_balances",
|
|
284
|
+
),
|
|
285
|
+
DjangoQ2ScheduleConfig(
|
|
286
|
+
name="Cleanup old data daily",
|
|
287
|
+
schedule_type="cron",
|
|
288
|
+
cron="0 2 * * *", # 2 AM daily
|
|
289
|
+
command="cleanup_old_data",
|
|
290
|
+
command_kwargs={"days": 30},
|
|
291
|
+
),
|
|
292
|
+
DjangoQ2ScheduleConfig(
|
|
293
|
+
name="Quick check every 5 minutes",
|
|
294
|
+
schedule_type="minutes",
|
|
295
|
+
minutes=5,
|
|
296
|
+
command="health_check",
|
|
297
|
+
),
|
|
298
|
+
],
|
|
299
|
+
)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Admin interface:
|
|
303
|
+
- Visit /admin/django_q/ to view tasks and schedules
|
|
304
|
+
- Monitor task execution, failures, and performance
|
|
305
|
+
- Manually trigger scheduled tasks
|
|
306
|
+
- View task results and logs
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
model_config = ConfigDict(
|
|
310
|
+
validate_assignment=True,
|
|
311
|
+
extra="forbid",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
enabled: bool = Field(
|
|
315
|
+
default=True,
|
|
316
|
+
description="Enable Django-Q (auto-adds django_q to INSTALLED_APPS)",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
schedules: List[DjangoQ2ScheduleConfig] = Field(
|
|
320
|
+
default_factory=list,
|
|
321
|
+
description="List of scheduled tasks",
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Django-Q broker configuration
|
|
325
|
+
broker_url: str = Field(
|
|
326
|
+
default="redis://localhost:6379/0",
|
|
327
|
+
description="Broker URL (Redis recommended for production)",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
broker_class: Literal["redis", "orm"] = Field(
|
|
331
|
+
default="redis",
|
|
332
|
+
description="Broker backend class (redis or orm)",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Queue configuration
|
|
336
|
+
queue_limit: Optional[int] = Field(
|
|
337
|
+
default=50,
|
|
338
|
+
ge=1,
|
|
339
|
+
description="Maximum tasks in queue before rejecting new ones",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
workers: int = Field(
|
|
343
|
+
default=4,
|
|
344
|
+
ge=1,
|
|
345
|
+
le=32,
|
|
346
|
+
description="Number of worker processes",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
timeout: int = Field(
|
|
350
|
+
default=300,
|
|
351
|
+
ge=1,
|
|
352
|
+
le=86400, # Max 24 hours
|
|
353
|
+
description="Default task timeout in seconds",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
retry: int = Field(
|
|
357
|
+
default=3600,
|
|
358
|
+
ge=0,
|
|
359
|
+
description="Seconds to wait before retrying failed tasks (0 = no retry)",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Task result configuration
|
|
363
|
+
save_limit: int = Field(
|
|
364
|
+
default=250,
|
|
365
|
+
ge=0,
|
|
366
|
+
description="Maximum number of successful tasks to save (0 = unlimited)",
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
cached: int = Field(
|
|
370
|
+
default=500,
|
|
371
|
+
ge=0,
|
|
372
|
+
description="Maximum number of tasks to cache (0 = disabled)",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Monitoring
|
|
376
|
+
monitor_interval: int = Field(
|
|
377
|
+
default=30,
|
|
378
|
+
ge=1,
|
|
379
|
+
description="Seconds between monitor checks",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Logging
|
|
383
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(
|
|
384
|
+
default="INFO",
|
|
385
|
+
description="Django-Q log level",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Advanced options
|
|
389
|
+
compress: bool = Field(
|
|
390
|
+
default=False,
|
|
391
|
+
description="Compress task data",
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
catch_up: bool = Field(
|
|
395
|
+
default=True,
|
|
396
|
+
description="Run missed scheduled tasks immediately",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
sync: bool = Field(
|
|
400
|
+
default=False,
|
|
401
|
+
description="Run tasks synchronously (for testing)",
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
def get_enabled_schedules(self) -> List[DjangoQ2ScheduleConfig]:
|
|
405
|
+
"""Get list of enabled schedules."""
|
|
406
|
+
return [schedule for schedule in self.schedules if schedule.enabled]
|
|
407
|
+
|
|
408
|
+
def to_django_settings(self, parent_config: Optional[Any] = None) -> Dict[str, Any]:
|
|
409
|
+
"""
|
|
410
|
+
Convert to Django settings dictionary.
|
|
411
|
+
|
|
412
|
+
Generates Q_CLUSTER configuration for Django-Q2.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
parent_config: Optional parent DjangoConfig for accessing redis_url
|
|
416
|
+
|
|
417
|
+
Note: Schedules are created via Django ORM, not settings.
|
|
418
|
+
Use management command: python manage.py qcluster
|
|
419
|
+
"""
|
|
420
|
+
if not self.enabled:
|
|
421
|
+
return {}
|
|
422
|
+
|
|
423
|
+
# Auto-detect redis_url from parent config if not explicitly set
|
|
424
|
+
broker_url = self.broker_url
|
|
425
|
+
if broker_url == "redis://localhost:6379/0" and parent_config:
|
|
426
|
+
# Use redis_url from parent config if available
|
|
427
|
+
if hasattr(parent_config, 'redis_url') and parent_config.redis_url:
|
|
428
|
+
broker_url = parent_config.redis_url
|
|
429
|
+
|
|
430
|
+
# Map short broker names to full class paths
|
|
431
|
+
broker_class_map = {
|
|
432
|
+
"redis": "django_q.brokers.redis_broker.Redis",
|
|
433
|
+
"orm": "django_q.brokers.orm.ORM",
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
cluster_config = {
|
|
437
|
+
# Broker
|
|
438
|
+
"name": "django_cfg_cluster",
|
|
439
|
+
"broker": broker_url,
|
|
440
|
+
"broker_class": broker_class_map.get(self.broker_class, self.broker_class),
|
|
441
|
+
|
|
442
|
+
# Queue
|
|
443
|
+
"queue_limit": self.queue_limit,
|
|
444
|
+
"workers": self.workers,
|
|
445
|
+
"timeout": self.timeout,
|
|
446
|
+
"retry": self.retry,
|
|
447
|
+
|
|
448
|
+
# Results
|
|
449
|
+
"save_limit": self.save_limit,
|
|
450
|
+
"cached": self.cached,
|
|
451
|
+
|
|
452
|
+
# Monitoring
|
|
453
|
+
"monitor": self.monitor_interval,
|
|
454
|
+
|
|
455
|
+
# Logging
|
|
456
|
+
"log_level": self.log_level,
|
|
457
|
+
|
|
458
|
+
# Advanced
|
|
459
|
+
"compress": self.compress,
|
|
460
|
+
"catch_up": self.catch_up,
|
|
461
|
+
"sync": self.sync,
|
|
462
|
+
|
|
463
|
+
# Django integration
|
|
464
|
+
"orm": "default",
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
# Only add django_redis if broker is actually Redis
|
|
468
|
+
if self.broker_class == "redis":
|
|
469
|
+
# Don't set django_redis - let Django-Q2 connect directly via broker URL
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
settings = {
|
|
473
|
+
"Q_CLUSTER": cluster_config
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return settings
|
|
477
|
+
|
|
478
|
+
def get_schedule_by_name(self, name: str) -> Optional[DjangoQ2ScheduleConfig]:
|
|
479
|
+
"""Get schedule by name."""
|
|
480
|
+
for schedule in self.schedules:
|
|
481
|
+
if schedule.name == name:
|
|
482
|
+
return schedule
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
def get_schedules_by_command(self, command: str) -> List[DjangoQ2ScheduleConfig]:
|
|
486
|
+
"""Get all schedules for a specific command."""
|
|
487
|
+
return [
|
|
488
|
+
schedule for schedule in self.schedules
|
|
489
|
+
if schedule.command == command
|
|
490
|
+
]
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
__all__ = [
|
|
494
|
+
"DjangoQ2ScheduleConfig",
|
|
495
|
+
"DjangoQ2Config",
|
|
496
|
+
]
|