django-cfg 1.5.1__py3-none-any.whl → 1.5.2__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.2.dist-info}/METADATA +5 -6
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.2.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.2.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.2.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DRF serializers for RQ testing and simulation endpoints.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rest_framework import serializers
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestScenarioSerializer(serializers.Serializer):
|
|
9
|
+
"""Serializer for available test scenarios."""
|
|
10
|
+
|
|
11
|
+
id = serializers.CharField(help_text="Scenario ID")
|
|
12
|
+
name = serializers.CharField(help_text="Scenario name")
|
|
13
|
+
description = serializers.CharField(help_text="Scenario description")
|
|
14
|
+
task_func = serializers.CharField(help_text="Task function path")
|
|
15
|
+
default_args = serializers.ListField(
|
|
16
|
+
child=serializers.JSONField(),
|
|
17
|
+
required=False,
|
|
18
|
+
default=list,
|
|
19
|
+
help_text="Default arguments"
|
|
20
|
+
)
|
|
21
|
+
default_kwargs = serializers.DictField(
|
|
22
|
+
required=False,
|
|
23
|
+
default=dict,
|
|
24
|
+
help_text="Default keyword arguments"
|
|
25
|
+
)
|
|
26
|
+
estimated_duration = serializers.IntegerField(
|
|
27
|
+
required=False,
|
|
28
|
+
allow_null=True,
|
|
29
|
+
help_text="Estimated duration in seconds"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RunDemoRequestSerializer(serializers.Serializer):
|
|
34
|
+
"""Serializer for running demo tasks."""
|
|
35
|
+
|
|
36
|
+
scenario = serializers.ChoiceField(
|
|
37
|
+
choices=[
|
|
38
|
+
'success',
|
|
39
|
+
'failure',
|
|
40
|
+
'slow',
|
|
41
|
+
'progress',
|
|
42
|
+
'retry',
|
|
43
|
+
'random',
|
|
44
|
+
'memory',
|
|
45
|
+
'cpu',
|
|
46
|
+
],
|
|
47
|
+
help_text="Demo scenario to run"
|
|
48
|
+
)
|
|
49
|
+
queue = serializers.CharField(
|
|
50
|
+
default='default',
|
|
51
|
+
help_text="Queue name"
|
|
52
|
+
)
|
|
53
|
+
args = serializers.ListField(
|
|
54
|
+
child=serializers.JSONField(),
|
|
55
|
+
required=False,
|
|
56
|
+
default=list,
|
|
57
|
+
help_text="Task arguments"
|
|
58
|
+
)
|
|
59
|
+
kwargs = serializers.DictField(
|
|
60
|
+
required=False,
|
|
61
|
+
default=dict,
|
|
62
|
+
help_text="Task keyword arguments"
|
|
63
|
+
)
|
|
64
|
+
timeout = serializers.IntegerField(
|
|
65
|
+
required=False,
|
|
66
|
+
allow_null=True,
|
|
67
|
+
help_text="Job timeout in seconds"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class StressTestRequestSerializer(serializers.Serializer):
|
|
72
|
+
"""Serializer for stress testing."""
|
|
73
|
+
|
|
74
|
+
num_jobs = serializers.IntegerField(
|
|
75
|
+
min_value=1,
|
|
76
|
+
max_value=1000,
|
|
77
|
+
default=10,
|
|
78
|
+
help_text="Number of jobs to create"
|
|
79
|
+
)
|
|
80
|
+
queue = serializers.CharField(
|
|
81
|
+
default='default',
|
|
82
|
+
help_text="Queue name"
|
|
83
|
+
)
|
|
84
|
+
scenario = serializers.ChoiceField(
|
|
85
|
+
choices=['success', 'failure', 'slow', 'random'],
|
|
86
|
+
default='success',
|
|
87
|
+
help_text="Task scenario"
|
|
88
|
+
)
|
|
89
|
+
duration = serializers.IntegerField(
|
|
90
|
+
min_value=1,
|
|
91
|
+
max_value=60,
|
|
92
|
+
default=2,
|
|
93
|
+
help_text="Task duration in seconds"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestingActionResponseSerializer(serializers.Serializer):
|
|
98
|
+
"""Serializer for testing action responses."""
|
|
99
|
+
|
|
100
|
+
success = serializers.BooleanField(help_text="Action success status")
|
|
101
|
+
message = serializers.CharField(help_text="Action message")
|
|
102
|
+
job_ids = serializers.ListField(
|
|
103
|
+
child=serializers.CharField(),
|
|
104
|
+
required=False,
|
|
105
|
+
default=list,
|
|
106
|
+
help_text="Created job IDs"
|
|
107
|
+
)
|
|
108
|
+
count = serializers.IntegerField(
|
|
109
|
+
required=False,
|
|
110
|
+
allow_null=True,
|
|
111
|
+
help_text="Number of items affected"
|
|
112
|
+
)
|
|
113
|
+
metadata = serializers.DictField(
|
|
114
|
+
required=False,
|
|
115
|
+
default=dict,
|
|
116
|
+
help_text="Additional metadata"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class CleanupRequestSerializer(serializers.Serializer):
|
|
121
|
+
"""Serializer for cleanup operations."""
|
|
122
|
+
|
|
123
|
+
queue = serializers.CharField(
|
|
124
|
+
required=False,
|
|
125
|
+
allow_blank=True,
|
|
126
|
+
help_text="Queue name (empty for all queues)"
|
|
127
|
+
)
|
|
128
|
+
registries = serializers.ListField(
|
|
129
|
+
child=serializers.ChoiceField(
|
|
130
|
+
choices=['failed', 'finished', 'deferred', 'scheduled']
|
|
131
|
+
),
|
|
132
|
+
required=False,
|
|
133
|
+
default=list,
|
|
134
|
+
help_text="Registries to clean"
|
|
135
|
+
)
|
|
136
|
+
delete_demo_jobs_only = serializers.BooleanField(
|
|
137
|
+
default=True,
|
|
138
|
+
help_text="Only delete demo jobs (func starts with 'demo_')"
|
|
139
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worker information serializers for Django-RQ.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rest_framework import serializers
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WorkerSerializer(serializers.Serializer):
|
|
9
|
+
"""
|
|
10
|
+
Worker information serializer.
|
|
11
|
+
|
|
12
|
+
Provides detailed information about an RQ worker.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = serializers.CharField(help_text="Worker name/ID")
|
|
16
|
+
queues = serializers.ListField(
|
|
17
|
+
child=serializers.CharField(), default=list, help_text="List of queue names"
|
|
18
|
+
)
|
|
19
|
+
state = serializers.CharField(help_text="Worker state (idle/busy/suspended)")
|
|
20
|
+
current_job = serializers.CharField(
|
|
21
|
+
allow_null=True, required=False, help_text="Current job ID if busy"
|
|
22
|
+
)
|
|
23
|
+
birth = serializers.DateTimeField(help_text="Worker start time")
|
|
24
|
+
last_heartbeat = serializers.DateTimeField(help_text="Last heartbeat timestamp")
|
|
25
|
+
successful_job_count = serializers.IntegerField(
|
|
26
|
+
default=0, help_text="Total successful jobs"
|
|
27
|
+
)
|
|
28
|
+
failed_job_count = serializers.IntegerField(default=0, help_text="Total failed jobs")
|
|
29
|
+
total_working_time = serializers.FloatField(
|
|
30
|
+
default=0.0, help_text="Total working time in seconds"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WorkerStatsSerializer(serializers.Serializer):
|
|
35
|
+
"""
|
|
36
|
+
Aggregated worker statistics serializer.
|
|
37
|
+
|
|
38
|
+
Provides overview of all workers across all queues.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
total_workers = serializers.IntegerField(help_text="Total number of workers")
|
|
42
|
+
busy_workers = serializers.IntegerField(default=0, help_text="Number of busy workers")
|
|
43
|
+
idle_workers = serializers.IntegerField(default=0, help_text="Number of idle workers")
|
|
44
|
+
suspended_workers = serializers.IntegerField(
|
|
45
|
+
default=0, help_text="Number of suspended workers"
|
|
46
|
+
)
|
|
47
|
+
total_successful_jobs = serializers.IntegerField(
|
|
48
|
+
default=0, help_text="Total successful jobs (all workers)"
|
|
49
|
+
)
|
|
50
|
+
total_failed_jobs = serializers.IntegerField(
|
|
51
|
+
default=0, help_text="Total failed jobs (all workers)"
|
|
52
|
+
)
|
|
53
|
+
total_working_time = serializers.FloatField(
|
|
54
|
+
default=0.0, help_text="Total working time across all workers (seconds)"
|
|
55
|
+
)
|
|
56
|
+
workers = serializers.ListField(
|
|
57
|
+
child=WorkerSerializer(), default=list, help_text="List of individual workers"
|
|
58
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Services for Django-RQ monitoring.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .config_helper import (
|
|
6
|
+
get_redis_url,
|
|
7
|
+
get_rq_config,
|
|
8
|
+
is_prometheus_enabled,
|
|
9
|
+
is_rq_enabled,
|
|
10
|
+
register_schedules_from_config,
|
|
11
|
+
)
|
|
12
|
+
from .rq_converters import job_to_model, queue_to_model, worker_to_model
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
# Converters
|
|
16
|
+
'job_to_model',
|
|
17
|
+
'queue_to_model',
|
|
18
|
+
'worker_to_model',
|
|
19
|
+
# Config helpers
|
|
20
|
+
'get_redis_url',
|
|
21
|
+
'get_rq_config',
|
|
22
|
+
'is_rq_enabled',
|
|
23
|
+
'is_prometheus_enabled',
|
|
24
|
+
'register_schedules_from_config',
|
|
25
|
+
]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper functions for accessing Django-RQ configuration from django-cfg.
|
|
3
|
+
|
|
4
|
+
Provides utilities to get RQ config and check if RQ is enabled.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from django_cfg.modules.django_logging import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger("rq.config")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_rq_config() -> Optional["DjangoRQConfig"]:
|
|
15
|
+
"""
|
|
16
|
+
Get Django-RQ configuration from django-cfg.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
DjangoRQConfig instance or None if not configured
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
>>> config = get_rq_config()
|
|
23
|
+
>>> if config and config.enabled:
|
|
24
|
+
>>> print(config.queues)
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
from django_cfg.core.config import get_current_config
|
|
28
|
+
from django_cfg.models.django.django_rq import DjangoRQConfig
|
|
29
|
+
|
|
30
|
+
config = get_current_config()
|
|
31
|
+
if not config:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
django_rq = getattr(config, 'django_rq', None)
|
|
35
|
+
|
|
36
|
+
# Type validation
|
|
37
|
+
if django_rq and isinstance(django_rq, DjangoRQConfig):
|
|
38
|
+
return django_rq
|
|
39
|
+
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.debug(f"Failed to get RQ config: {e}")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_rq_enabled() -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Check if Django-RQ is enabled in django-cfg.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
True if RQ is enabled, False otherwise
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> if is_rq_enabled():
|
|
56
|
+
>>> from django_rq import enqueue
|
|
57
|
+
>>> enqueue(my_task)
|
|
58
|
+
"""
|
|
59
|
+
config = get_rq_config()
|
|
60
|
+
if not config:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
return getattr(config, 'enabled', False)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_queue_names() -> list:
|
|
67
|
+
"""
|
|
68
|
+
Get list of configured queue names.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of queue names from config
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> queues = get_queue_names()
|
|
75
|
+
>>> print(queues) # ['default', 'high', 'low']
|
|
76
|
+
"""
|
|
77
|
+
config = get_rq_config()
|
|
78
|
+
if not config:
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
queues = getattr(config, 'queues', {})
|
|
82
|
+
if isinstance(queues, dict):
|
|
83
|
+
return list(queues.keys())
|
|
84
|
+
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_prometheus_enabled() -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Check if Prometheus metrics export is enabled.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if Prometheus is enabled, False otherwise
|
|
94
|
+
"""
|
|
95
|
+
config = get_rq_config()
|
|
96
|
+
if not config:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
return getattr(config, 'prometheus_enabled', True)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_redis_url() -> Optional[str]:
|
|
103
|
+
"""
|
|
104
|
+
Get Redis URL from django-cfg DjangoConfig.
|
|
105
|
+
|
|
106
|
+
This is the global Redis URL that is automatically used for:
|
|
107
|
+
- RQ queues (if queue.url is not set)
|
|
108
|
+
- RQ scheduler
|
|
109
|
+
- Cache backend
|
|
110
|
+
- Session backend
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Redis URL string (e.g., "redis://localhost:6379/0") or None
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> redis_url = get_redis_url()
|
|
117
|
+
>>> print(redis_url) # redis://localhost:6379/0
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
from django_cfg.core.config import get_current_config
|
|
121
|
+
|
|
122
|
+
config = get_current_config()
|
|
123
|
+
if not config:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
return getattr(config, 'redis_url', None)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.debug(f"Failed to get redis_url: {e}")
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def register_schedules_from_config():
|
|
134
|
+
"""
|
|
135
|
+
Register scheduled jobs from django-cfg config in rq-scheduler.
|
|
136
|
+
|
|
137
|
+
This function should be called on Django startup (from AppConfig.ready()).
|
|
138
|
+
It reads schedules from config.django_rq.schedules and registers them
|
|
139
|
+
in rq-scheduler.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> from django_cfg.apps.rq.services import register_schedules_from_config
|
|
143
|
+
>>> register_schedules_from_config()
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
import django_rq
|
|
147
|
+
from rq_scheduler import Scheduler
|
|
148
|
+
|
|
149
|
+
config = get_rq_config()
|
|
150
|
+
if not config or not config.enabled:
|
|
151
|
+
logger.debug("RQ not enabled, skipping schedule registration")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
schedules = getattr(config, 'schedules', [])
|
|
155
|
+
if not schedules:
|
|
156
|
+
logger.debug("No schedules configured")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
# Get scheduler for default queue
|
|
160
|
+
queue = django_rq.get_queue('default')
|
|
161
|
+
scheduler = Scheduler(queue=queue, connection=queue.connection)
|
|
162
|
+
|
|
163
|
+
logger.info(f"Registering {len(schedules)} scheduled jobs from config...")
|
|
164
|
+
|
|
165
|
+
for schedule_config in schedules:
|
|
166
|
+
try:
|
|
167
|
+
# Import function
|
|
168
|
+
func_path = schedule_config.func
|
|
169
|
+
module_path, func_name = func_path.rsplit('.', 1)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
import importlib
|
|
173
|
+
module = importlib.import_module(module_path)
|
|
174
|
+
func = getattr(module, func_name)
|
|
175
|
+
except (ImportError, AttributeError) as e:
|
|
176
|
+
logger.warning(f"Failed to import function {func_path}: {e}")
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# Get schedule type and register
|
|
180
|
+
if schedule_config.cron:
|
|
181
|
+
scheduler.cron(
|
|
182
|
+
schedule_config.cron,
|
|
183
|
+
func=func,
|
|
184
|
+
args=schedule_config.args,
|
|
185
|
+
kwargs=schedule_config.kwargs,
|
|
186
|
+
queue_name=schedule_config.queue,
|
|
187
|
+
timeout=schedule_config.timeout,
|
|
188
|
+
result_ttl=schedule_config.result_ttl,
|
|
189
|
+
id=schedule_config.job_id,
|
|
190
|
+
repeat=schedule_config.repeat,
|
|
191
|
+
)
|
|
192
|
+
logger.info(f"✓ Registered cron schedule: {func_path} ({schedule_config.cron})")
|
|
193
|
+
|
|
194
|
+
elif schedule_config.interval:
|
|
195
|
+
from datetime import datetime
|
|
196
|
+
scheduler.schedule(
|
|
197
|
+
scheduled_time=datetime.utcnow(), # Start immediately
|
|
198
|
+
func=func,
|
|
199
|
+
args=schedule_config.args,
|
|
200
|
+
kwargs=schedule_config.kwargs,
|
|
201
|
+
interval=schedule_config.interval,
|
|
202
|
+
queue_name=schedule_config.queue,
|
|
203
|
+
timeout=schedule_config.timeout,
|
|
204
|
+
result_ttl=schedule_config.result_ttl,
|
|
205
|
+
id=schedule_config.job_id,
|
|
206
|
+
repeat=schedule_config.repeat,
|
|
207
|
+
)
|
|
208
|
+
logger.info(f"✓ Registered interval schedule: {func_path} (every {schedule_config.interval}s)")
|
|
209
|
+
|
|
210
|
+
elif schedule_config.scheduled_time:
|
|
211
|
+
from datetime import datetime
|
|
212
|
+
scheduled_dt = datetime.fromisoformat(schedule_config.scheduled_time)
|
|
213
|
+
|
|
214
|
+
scheduler.schedule(
|
|
215
|
+
scheduled_time=scheduled_dt,
|
|
216
|
+
func=func,
|
|
217
|
+
args=schedule_config.args,
|
|
218
|
+
kwargs=schedule_config.kwargs,
|
|
219
|
+
queue_name=schedule_config.queue,
|
|
220
|
+
timeout=schedule_config.timeout,
|
|
221
|
+
result_ttl=schedule_config.result_ttl,
|
|
222
|
+
id=schedule_config.job_id,
|
|
223
|
+
)
|
|
224
|
+
logger.info(f"✓ Registered one-time schedule: {func_path} (at {schedule_config.scheduled_time})")
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Failed to register schedule {schedule_config.func}: {e}")
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
logger.info("Schedule registration completed")
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Failed to register schedules: {e}", exc_info=True)
|