django-cfg 1.1.59__py3-none-any.whl → 1.1.62__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.
- django_cfg/__init__.py +1 -1
- django_cfg/management/commands/rundramatiq.py +193 -196
- django_cfg/modules/django_tasks.py +64 -497
- django_cfg/modules/dramatiq_setup.py +16 -0
- {django_cfg-1.1.59.dist-info → django_cfg-1.1.62.dist-info}/METADATA +1 -1
- {django_cfg-1.1.59.dist-info → django_cfg-1.1.62.dist-info}/RECORD +9 -8
- {django_cfg-1.1.59.dist-info → django_cfg-1.1.62.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.59.dist-info → django_cfg-1.1.62.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.59.dist-info → django_cfg-1.1.62.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
@@ -38,7 +38,7 @@ default_app_config = "django_cfg.apps.DjangoCfgConfig"
|
|
38
38
|
from typing import TYPE_CHECKING
|
39
39
|
|
40
40
|
# Version information
|
41
|
-
__version__ = "1.1.
|
41
|
+
__version__ = "1.1.62"
|
42
42
|
__author__ = "Unrealos Team"
|
43
43
|
__email__ = "info@unrealos.com"
|
44
44
|
__license__ = "MIT"
|
@@ -1,244 +1,241 @@
|
|
1
1
|
"""
|
2
2
|
Django management command for running Dramatiq workers.
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
Based on django_dramatiq.management.commands.rundramatiq with Django-CFG integration.
|
5
|
+
Simple, clean, and working approach.
|
6
6
|
"""
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
import logging
|
12
|
-
import sys
|
8
|
+
import argparse
|
9
|
+
import importlib
|
10
|
+
import multiprocessing
|
13
11
|
import os
|
12
|
+
import sys
|
13
|
+
|
14
|
+
from django.apps import apps
|
15
|
+
from django.conf import settings
|
16
|
+
from django.core.management.base import BaseCommand
|
17
|
+
from django.utils.module_loading import module_has_submodule
|
14
18
|
|
15
19
|
from django_cfg.modules.django_tasks import get_task_service
|
16
20
|
|
17
|
-
|
21
|
+
|
22
|
+
# Default values
|
23
|
+
NPROCS = multiprocessing.cpu_count()
|
24
|
+
NTHREADS = 8
|
18
25
|
|
19
26
|
|
20
27
|
class Command(BaseCommand):
|
21
|
-
""
|
22
|
-
|
23
|
-
|
24
|
-
This command starts Dramatiq workers using the configuration
|
25
|
-
defined in Django-CFG TaskConfig, with support for custom
|
26
|
-
process counts, queue selection, and worker options.
|
27
|
-
"""
|
28
|
-
|
29
|
-
help = "Run Dramatiq workers for background task processing"
|
30
|
-
|
28
|
+
help = "Run Dramatiq workers with Django-CFG configuration."
|
29
|
+
|
31
30
|
def add_arguments(self, parser):
|
32
|
-
|
31
|
+
parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
32
|
+
|
33
33
|
parser.add_argument(
|
34
|
-
"--processes",
|
34
|
+
"--processes", "-p",
|
35
|
+
default=NPROCS,
|
35
36
|
type=int,
|
36
|
-
help="
|
37
|
+
help="The number of processes to run",
|
37
38
|
)
|
38
39
|
parser.add_argument(
|
39
|
-
"--threads",
|
40
|
+
"--threads", "-t",
|
41
|
+
default=NTHREADS,
|
40
42
|
type=int,
|
41
|
-
help="
|
43
|
+
help="The number of threads per process to use",
|
42
44
|
)
|
43
45
|
parser.add_argument(
|
44
|
-
"--queues",
|
46
|
+
"--queues", "-Q",
|
47
|
+
nargs="*",
|
45
48
|
type=str,
|
46
|
-
help="
|
49
|
+
help="Listen to a subset of queues, or all when empty",
|
47
50
|
)
|
48
51
|
parser.add_argument(
|
49
|
-
"--
|
50
|
-
|
51
|
-
help="
|
52
|
+
"--watch",
|
53
|
+
dest="watch_dir",
|
54
|
+
help="Reload workers when changes are detected in the given directory",
|
52
55
|
)
|
53
56
|
parser.add_argument(
|
54
|
-
"--
|
57
|
+
"--pid-file",
|
55
58
|
type=str,
|
56
|
-
help="
|
59
|
+
help="Write the PID of the master process to this file",
|
57
60
|
)
|
58
61
|
parser.add_argument(
|
59
|
-
"--
|
62
|
+
"--log-file",
|
60
63
|
type=str,
|
61
|
-
help="Write
|
64
|
+
help="Write all logs to a file, or stderr when empty",
|
65
|
+
)
|
66
|
+
parser.add_argument(
|
67
|
+
"--worker-shutdown-timeout",
|
68
|
+
type=int,
|
69
|
+
default=600000,
|
70
|
+
help="Timeout for worker shutdown, in milliseconds"
|
62
71
|
)
|
63
72
|
parser.add_argument(
|
64
73
|
"--dry-run",
|
65
74
|
action="store_true",
|
66
75
|
help="Show configuration without starting workers",
|
67
76
|
)
|
68
|
-
|
69
|
-
def handle(self,
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
77
|
+
|
78
|
+
def handle(self, watch_dir, processes, threads, verbosity, queues,
|
79
|
+
pid_file, log_file, worker_shutdown_timeout, dry_run, **options):
|
80
|
+
|
81
|
+
# Get task service and validate
|
82
|
+
task_service = get_task_service()
|
83
|
+
if not task_service.is_enabled():
|
84
|
+
self.stdout.write(
|
85
|
+
self.style.ERROR("Task system is not enabled in Django-CFG configuration")
|
86
|
+
)
|
87
|
+
return
|
88
|
+
|
89
|
+
# Discover task modules
|
90
|
+
tasks_modules = self._discover_tasks_modules()
|
91
|
+
|
92
|
+
# Show configuration info
|
93
|
+
self.stdout.write(self.style.SUCCESS("Dramatiq Worker Configuration:"))
|
94
|
+
self.stdout.write(f"Processes: {processes}")
|
95
|
+
self.stdout.write(f"Threads: {threads}")
|
96
|
+
if queues:
|
97
|
+
self.stdout.write(f"Queues: {', '.join(queues)}")
|
98
|
+
else:
|
99
|
+
self.stdout.write("Queues: all")
|
100
|
+
|
101
|
+
self.stdout.write(f"\nDiscovered task modules:")
|
102
|
+
for module in tasks_modules:
|
103
|
+
self.stdout.write(f" - {module}")
|
104
|
+
|
105
|
+
# If dry run, show command and exit
|
106
|
+
if dry_run:
|
107
|
+
executable_name = "dramatiq"
|
108
|
+
process_args = [
|
109
|
+
executable_name,
|
110
|
+
"--processes", str(processes),
|
111
|
+
"--threads", str(threads),
|
112
|
+
"--worker-shutdown-timeout", str(worker_shutdown_timeout),
|
113
|
+
]
|
74
114
|
|
75
|
-
|
76
|
-
|
77
|
-
raise CommandError(
|
78
|
-
"Task system is not enabled. "
|
79
|
-
"Please configure 'tasks' in your Django-CFG configuration."
|
80
|
-
)
|
115
|
+
if watch_dir:
|
116
|
+
process_args.extend(["--watch", watch_dir])
|
81
117
|
|
82
|
-
|
83
|
-
|
84
|
-
raise CommandError(
|
85
|
-
"Task system configuration is invalid. "
|
86
|
-
"Please check your Redis connection and task settings."
|
87
|
-
)
|
118
|
+
verbosity_args = ["-v"] * (verbosity - 1)
|
119
|
+
process_args.extend(verbosity_args)
|
88
120
|
|
89
|
-
|
90
|
-
|
91
|
-
if not config:
|
92
|
-
raise CommandError("Task configuration not available")
|
121
|
+
if queues:
|
122
|
+
process_args.extend(["--queues", *queues])
|
93
123
|
|
94
|
-
|
95
|
-
|
124
|
+
if pid_file:
|
125
|
+
process_args.extend(["--pid-file", pid_file])
|
96
126
|
|
97
|
-
if
|
98
|
-
|
99
|
-
return
|
127
|
+
if log_file:
|
128
|
+
process_args.extend(["--log-file", log_file])
|
100
129
|
|
101
|
-
|
102
|
-
self._start_workers(worker_args, options)
|
130
|
+
process_args.extend(tasks_modules)
|
103
131
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
"Install with: pip install django-cfg[tasks]"
|
108
|
-
)
|
109
|
-
except Exception as e:
|
110
|
-
logger.exception("Failed to start Dramatiq workers")
|
111
|
-
raise CommandError(f"Failed to start workers: {e}")
|
112
|
-
|
113
|
-
def _build_worker_args(self, config, options) -> List[str]:
|
114
|
-
"""Build command line arguments for Dramatiq workers."""
|
115
|
-
args = ["dramatiq"]
|
116
|
-
|
117
|
-
# Process count
|
118
|
-
processes = options.get("processes") or config.get_effective_processes()
|
119
|
-
args.extend(["--processes", str(processes)])
|
120
|
-
|
121
|
-
# Thread count
|
122
|
-
threads = options.get("threads") or config.dramatiq.threads
|
123
|
-
args.extend(["--threads", str(threads)])
|
124
|
-
|
125
|
-
# Queues
|
126
|
-
if options.get("queues"):
|
127
|
-
queues = [q.strip() for q in options["queues"].split(",")]
|
128
|
-
else:
|
129
|
-
queues = config.get_effective_queues()
|
130
|
-
|
131
|
-
for queue in queues:
|
132
|
-
args.extend(["--queues", queue])
|
133
|
-
|
134
|
-
# Log level (not supported by standard dramatiq CLI)
|
135
|
-
# log_level = options.get("log_level") or config.worker.log_level
|
136
|
-
# args.extend(["--log-level", log_level])
|
137
|
-
|
138
|
-
# Watch directory
|
139
|
-
if options.get("watch"):
|
140
|
-
args.extend(["--watch", options["watch"]])
|
141
|
-
|
142
|
-
# PID file
|
143
|
-
if options.get("pid_file"):
|
144
|
-
args.extend(["--pid-file", options["pid_file"]])
|
145
|
-
|
146
|
-
# Note: Using Python API instead of CLI, so no broker argument needed
|
147
|
-
|
148
|
-
# Add discovered task modules
|
149
|
-
discovered_modules = get_task_service().discover_tasks()
|
150
|
-
for module in discovered_modules:
|
151
|
-
args.append(module)
|
152
|
-
|
153
|
-
# If no modules discovered, add default patterns
|
154
|
-
if not discovered_modules:
|
155
|
-
# Add common task module patterns
|
156
|
-
for app in settings.INSTALLED_APPS:
|
157
|
-
if not app.startswith("django.") and not app.startswith("django_"):
|
158
|
-
args.append(f"{app}.tasks")
|
159
|
-
|
160
|
-
return args
|
161
|
-
|
162
|
-
def _show_configuration(self, config, worker_args):
|
163
|
-
"""Show worker configuration without starting."""
|
164
|
-
self.stdout.write(
|
165
|
-
self.style.SUCCESS("Dramatiq Worker Configuration:")
|
166
|
-
)
|
167
|
-
|
168
|
-
self.stdout.write(f" Processes: {config.get_effective_processes()}")
|
169
|
-
self.stdout.write(f" Threads: {config.dramatiq.threads}")
|
170
|
-
self.stdout.write(f" Queues: {', '.join(config.get_effective_queues())}")
|
171
|
-
self.stdout.write(f" Log Level: {config.worker.log_level}")
|
172
|
-
self.stdout.write(f" Redis DB: {config.dramatiq.redis_db}")
|
173
|
-
|
174
|
-
self.stdout.write("\nDiscovered task modules:")
|
175
|
-
discovered = get_task_service().discover_tasks()
|
176
|
-
if discovered:
|
177
|
-
for module in discovered:
|
178
|
-
self.stdout.write(f" - {module}")
|
179
|
-
else:
|
180
|
-
self.stdout.write(" (none found)")
|
181
|
-
|
182
|
-
self.stdout.write(f"\nCommand that would be executed:")
|
183
|
-
self.stdout.write(f" {' '.join(worker_args)}")
|
184
|
-
|
185
|
-
def _start_workers(self, worker_args, options):
|
186
|
-
"""Start Dramatiq workers using Python API instead of CLI."""
|
187
|
-
self.stdout.write(
|
188
|
-
self.style.SUCCESS("Starting Dramatiq workers...")
|
189
|
-
)
|
132
|
+
self.stdout.write(f"\nCommand that would be executed:")
|
133
|
+
self.stdout.write(f' {" ".join(process_args)}')
|
134
|
+
return
|
190
135
|
|
191
136
|
# Show startup info
|
137
|
+
self.stdout.write(self.style.SUCCESS("\nStarting Dramatiq workers..."))
|
138
|
+
|
139
|
+
# Build dramatiq command
|
140
|
+
executable_name = "dramatiq"
|
141
|
+
executable_path = self._resolve_executable(executable_name)
|
142
|
+
|
143
|
+
# Build process arguments exactly like django_dramatiq
|
144
|
+
process_args = [
|
145
|
+
executable_name,
|
146
|
+
"--processes", str(processes),
|
147
|
+
"--threads", str(threads),
|
148
|
+
"--worker-shutdown-timeout", str(worker_shutdown_timeout),
|
149
|
+
]
|
150
|
+
|
151
|
+
# Add watch directory if specified
|
152
|
+
if watch_dir:
|
153
|
+
process_args.extend(["--watch", watch_dir])
|
154
|
+
|
155
|
+
# Add verbosity
|
156
|
+
verbosity_args = ["-v"] * (verbosity - 1)
|
157
|
+
process_args.extend(verbosity_args)
|
158
|
+
|
159
|
+
# Add queues if specified
|
160
|
+
if queues:
|
161
|
+
process_args.extend(["--queues", *queues])
|
162
|
+
|
163
|
+
# Add PID file if specified
|
164
|
+
if pid_file:
|
165
|
+
process_args.extend(["--pid-file", pid_file])
|
166
|
+
|
167
|
+
# Add log file if specified
|
168
|
+
if log_file:
|
169
|
+
process_args.extend(["--log-file", log_file])
|
170
|
+
|
171
|
+
# Add task modules (broker module first, then discovered modules)
|
172
|
+
process_args.extend(tasks_modules)
|
173
|
+
|
174
|
+
self.stdout.write(f'Running dramatiq: "{" ".join(process_args)}"\n')
|
175
|
+
|
176
|
+
# Ensure DJANGO_SETTINGS_MODULE is set for worker processes
|
177
|
+
if not os.environ.get('DJANGO_SETTINGS_MODULE'):
|
178
|
+
if hasattr(settings, 'SETTINGS_MODULE'):
|
179
|
+
os.environ['DJANGO_SETTINGS_MODULE'] = settings.SETTINGS_MODULE
|
180
|
+
else:
|
181
|
+
# Try to detect from manage.py or current settings
|
182
|
+
import django
|
183
|
+
from django.conf import settings as django_settings
|
184
|
+
if hasattr(django_settings, '_wrapped') and hasattr(django_settings._wrapped, '__module__'):
|
185
|
+
module_name = django_settings._wrapped.__module__
|
186
|
+
os.environ['DJANGO_SETTINGS_MODULE'] = module_name
|
187
|
+
else:
|
188
|
+
self.stdout.write(
|
189
|
+
self.style.WARNING("Could not detect DJANGO_SETTINGS_MODULE")
|
190
|
+
)
|
191
|
+
|
192
|
+
# Use os.execvp like django_dramatiq to preserve environment
|
193
|
+
if sys.platform == "win32":
|
194
|
+
import subprocess
|
195
|
+
command = [executable_path] + process_args[1:]
|
196
|
+
sys.exit(subprocess.run(command))
|
197
|
+
|
198
|
+
os.execvp(executable_path, process_args)
|
199
|
+
|
200
|
+
def _discover_tasks_modules(self):
|
201
|
+
"""Discover task modules like django_dramatiq does."""
|
202
|
+
# Always include our broker setup module first
|
203
|
+
tasks_modules = ["django_cfg.modules.dramatiq_setup"]
|
204
|
+
|
205
|
+
# Get task service for configuration
|
192
206
|
task_service = get_task_service()
|
193
|
-
config = task_service.config
|
194
207
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
208
|
+
# Try to get task modules from Django-CFG config
|
209
|
+
if task_service.config and task_service.config.auto_discover_tasks:
|
210
|
+
discovered = task_service.discover_tasks()
|
211
|
+
for module_name in discovered:
|
212
|
+
self.stdout.write(f"Discovered tasks module: '{module_name}'")
|
213
|
+
tasks_modules.append(module_name)
|
200
214
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
# Write PID file if requested
|
206
|
-
if options.get("pid_file"):
|
207
|
-
with open(options["pid_file"], "w") as f:
|
208
|
-
f.write(str(os.getpid()))
|
209
|
-
self.stdout.write(f"PID written to: {options['pid_file']}")
|
210
|
-
|
211
|
-
try:
|
212
|
-
# Import dramatiq and get broker FIRST
|
213
|
-
import dramatiq
|
214
|
-
from dramatiq.worker import Worker
|
215
|
-
from django_cfg.modules.django_tasks import get_dramatiq_broker
|
216
|
-
|
217
|
-
# Get the configured broker
|
218
|
-
broker = get_dramatiq_broker()
|
215
|
+
# Fallback: use django_dramatiq discovery logic
|
216
|
+
if len(tasks_modules) == 1: # Only broker module found
|
217
|
+
task_module_names = getattr(settings, "DRAMATIQ_AUTODISCOVER_MODULES", ("tasks",))
|
219
218
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
logger.exception("Worker execution failed")
|
244
|
-
raise CommandError(f"Worker execution failed: {e}")
|
219
|
+
for app_config in apps.get_app_configs():
|
220
|
+
for task_module in task_module_names:
|
221
|
+
if module_has_submodule(app_config.module, task_module):
|
222
|
+
module_name = f"{app_config.name}.{task_module}"
|
223
|
+
try:
|
224
|
+
importlib.import_module(module_name)
|
225
|
+
self.stdout.write(f"Discovered tasks module: '{module_name}'")
|
226
|
+
tasks_modules.append(module_name)
|
227
|
+
except ImportError:
|
228
|
+
# Module exists but has import errors, skip it
|
229
|
+
pass
|
230
|
+
|
231
|
+
return tasks_modules
|
232
|
+
|
233
|
+
def _resolve_executable(self, exec_name):
|
234
|
+
"""Resolve executable path like django_dramatiq does."""
|
235
|
+
bin_dir = os.path.dirname(sys.executable)
|
236
|
+
if bin_dir:
|
237
|
+
for d in [bin_dir, os.path.join(bin_dir, "Scripts")]:
|
238
|
+
exec_path = os.path.join(d, exec_name)
|
239
|
+
if os.path.isfile(exec_path):
|
240
|
+
return exec_path
|
241
|
+
return exec_name
|
@@ -1,18 +1,16 @@
|
|
1
1
|
"""
|
2
2
|
Django-CFG Task Service Module.
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
Simplified and focused task service for Dramatiq integration.
|
5
|
+
Provides essential functionality without unnecessary complexity.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from typing import Optional, Dict, Any, List
|
8
|
+
from typing import Optional, Dict, Any, List
|
9
9
|
import logging
|
10
|
-
import subprocess
|
11
|
-
import time
|
12
10
|
from urllib.parse import urlparse
|
13
11
|
|
14
12
|
from django_cfg.modules.base import BaseModule
|
15
|
-
from django_cfg.models.tasks import TaskConfig,
|
13
|
+
from django_cfg.models.tasks import TaskConfig, validate_task_config
|
16
14
|
from django_cfg.models.constance import ConstanceField
|
17
15
|
|
18
16
|
# Django imports (will be available when Django is configured)
|
@@ -26,15 +24,8 @@ except ImportError:
|
|
26
24
|
# Optional imports
|
27
25
|
try:
|
28
26
|
import dramatiq
|
29
|
-
from dramatiq.brokers.redis import RedisBroker
|
30
27
|
except ImportError:
|
31
28
|
dramatiq = None
|
32
|
-
RedisBroker = None
|
33
|
-
|
34
|
-
try:
|
35
|
-
import django_dramatiq
|
36
|
-
except ImportError:
|
37
|
-
django_dramatiq = None
|
38
29
|
|
39
30
|
try:
|
40
31
|
import redis
|
@@ -44,294 +35,121 @@ except ImportError:
|
|
44
35
|
logger = logging.getLogger(__name__)
|
45
36
|
|
46
37
|
|
47
|
-
class TaskManager:
|
48
|
-
"""
|
49
|
-
Task management and worker control.
|
50
|
-
|
51
|
-
Provides high-level interface for managing Dramatiq workers,
|
52
|
-
monitoring task queues, and controlling task execution.
|
53
|
-
"""
|
54
|
-
|
55
|
-
def __init__(self, config: DramatiqConfig):
|
56
|
-
self.config = config
|
57
|
-
self._broker = None
|
58
|
-
self._workers = []
|
59
|
-
|
60
|
-
@property
|
61
|
-
def broker(self):
|
62
|
-
"""Get Dramatiq broker instance (lazy-loaded)."""
|
63
|
-
if self._broker is None:
|
64
|
-
if dramatiq is None:
|
65
|
-
logger.error("Dramatiq not available")
|
66
|
-
return None
|
67
|
-
|
68
|
-
try:
|
69
|
-
# This will be configured by Django settings
|
70
|
-
self._broker = dramatiq.get_broker()
|
71
|
-
except Exception as e:
|
72
|
-
logger.error(f"Failed to get Dramatiq broker: {e}")
|
73
|
-
return None
|
74
|
-
|
75
|
-
return self._broker
|
76
|
-
|
77
|
-
def get_queue_stats(self) -> List[Dict[str, Any]]:
|
78
|
-
"""Get statistics for all configured queues."""
|
79
|
-
if not self.broker:
|
80
|
-
return []
|
81
|
-
|
82
|
-
stats = []
|
83
|
-
for queue_name in self.config.dramatiq.queues:
|
84
|
-
try:
|
85
|
-
# Get queue statistics from broker
|
86
|
-
queue_stats = {
|
87
|
-
"name": queue_name,
|
88
|
-
"pending": 0, # Will be populated by actual broker stats
|
89
|
-
"running": 0,
|
90
|
-
"completed": 0,
|
91
|
-
"failed": 0,
|
92
|
-
}
|
93
|
-
|
94
|
-
# TODO: Implement actual queue statistics retrieval
|
95
|
-
# This depends on the specific broker implementation
|
96
|
-
|
97
|
-
stats.append(queue_stats)
|
98
|
-
except Exception as e:
|
99
|
-
logger.error(f"Failed to get stats for queue {queue_name}: {e}")
|
100
|
-
|
101
|
-
return stats
|
102
|
-
|
103
|
-
def get_worker_stats(self) -> List[Dict[str, Any]]:
|
104
|
-
"""Get statistics for all active workers."""
|
105
|
-
# TODO: Implement worker statistics retrieval
|
106
|
-
# This would typically involve checking process status,
|
107
|
-
# memory usage, and current task information
|
108
|
-
|
109
|
-
return []
|
110
|
-
|
111
|
-
def clear_queue(self, queue_name: str) -> bool:
|
112
|
-
"""Clear all messages from a specific queue."""
|
113
|
-
if not self.broker:
|
114
|
-
return False
|
115
|
-
|
116
|
-
try:
|
117
|
-
# TODO: Implement queue clearing
|
118
|
-
logger.info(f"Cleared queue: {queue_name}")
|
119
|
-
return True
|
120
|
-
except Exception as e:
|
121
|
-
logger.error(f"Failed to clear queue {queue_name}: {e}")
|
122
|
-
return False
|
123
|
-
|
124
|
-
def retry_failed_tasks(self, queue_name: Optional[str] = None) -> int:
|
125
|
-
"""Retry failed tasks in specified queue or all queues."""
|
126
|
-
if not self.broker:
|
127
|
-
return 0
|
128
|
-
|
129
|
-
try:
|
130
|
-
# TODO: Implement failed task retry logic
|
131
|
-
retried_count = 0
|
132
|
-
logger.info(f"Retried {retried_count} failed tasks")
|
133
|
-
return retried_count
|
134
|
-
except Exception as e:
|
135
|
-
logger.error(f"Failed to retry tasks: {e}")
|
136
|
-
return 0
|
137
|
-
|
138
|
-
|
139
38
|
class DjangoTasks(BaseModule):
|
140
39
|
"""
|
141
|
-
|
40
|
+
Simplified Django-CFG task service.
|
142
41
|
|
143
|
-
|
144
|
-
|
42
|
+
Focuses on essential functionality:
|
43
|
+
- Configuration management
|
44
|
+
- Task discovery
|
45
|
+
- Health checks
|
46
|
+
- Constance integration
|
145
47
|
"""
|
146
48
|
|
147
49
|
def __init__(self):
|
148
50
|
super().__init__()
|
149
51
|
self._config: Optional[TaskConfig] = None
|
150
|
-
self._manager: Optional[TaskManager] = None
|
151
52
|
self._redis_url: Optional[str] = None
|
152
53
|
|
153
54
|
@property
|
154
55
|
def config(self) -> Optional[TaskConfig]:
|
155
56
|
"""Get task configuration (lazy-loaded)."""
|
156
|
-
|
157
|
-
try:
|
158
|
-
# First try the base class method
|
159
|
-
django_config = self.get_config() # This returns full DjangoConfig
|
160
|
-
if django_config and hasattr(django_config, 'tasks'):
|
161
|
-
task_config = django_config.tasks
|
162
|
-
if task_config and isinstance(task_config, TaskConfig):
|
163
|
-
# Update cache with fresh config
|
164
|
-
self._config = task_config
|
165
|
-
logger.debug(f"Loaded TaskConfig: enabled={task_config.enabled}")
|
166
|
-
return self._config
|
167
|
-
elif task_config is None:
|
168
|
-
logger.debug("Tasks configuration is None in Django config")
|
169
|
-
else:
|
170
|
-
logger.error(f"Expected TaskConfig, got {type(task_config)}")
|
171
|
-
else:
|
172
|
-
logger.debug("No tasks attribute found in Django config")
|
173
|
-
|
174
|
-
# Fallback: try to import config directly
|
175
|
-
try:
|
176
|
-
from api.config import config as api_config
|
177
|
-
if hasattr(api_config, 'tasks') and api_config.tasks:
|
178
|
-
task_config = api_config.tasks
|
179
|
-
if isinstance(task_config, TaskConfig):
|
180
|
-
self._config = task_config
|
181
|
-
logger.debug(f"Loaded TaskConfig from api.config: enabled={task_config.enabled}")
|
182
|
-
return self._config
|
183
|
-
except ImportError:
|
184
|
-
logger.debug("Could not import api.config")
|
185
|
-
|
186
|
-
return None
|
187
|
-
except Exception as e:
|
188
|
-
logger.warning(f"Failed to get task config: {e}")
|
189
|
-
# Fallback to cached version if available
|
190
|
-
return self._config
|
191
|
-
|
192
|
-
@property
|
193
|
-
def manager(self) -> Optional[TaskManager]:
|
194
|
-
"""Get task manager (lazy-loaded)."""
|
195
|
-
if self._manager is None and self.config:
|
57
|
+
if self._config is None:
|
196
58
|
try:
|
197
|
-
|
59
|
+
# Get config from django-cfg
|
60
|
+
django_config = self.get_config()
|
61
|
+
if django_config and hasattr(django_config, 'tasks'):
|
62
|
+
self._config = django_config.tasks
|
63
|
+
logger.debug(f"Loaded TaskConfig: enabled={self._config.enabled if self._config else False}")
|
64
|
+
else:
|
65
|
+
# Fallback: try direct import
|
66
|
+
try:
|
67
|
+
from api.config import config as api_config
|
68
|
+
if hasattr(api_config, 'tasks') and api_config.tasks:
|
69
|
+
self._config = api_config.tasks
|
70
|
+
logger.debug(f"Loaded TaskConfig from api.config: enabled={self._config.enabled}")
|
71
|
+
except ImportError:
|
72
|
+
logger.debug("Could not import api.config")
|
198
73
|
except Exception as e:
|
199
|
-
logger.
|
200
|
-
|
201
|
-
return self.
|
74
|
+
logger.warning(f"Failed to get task config: {e}")
|
75
|
+
|
76
|
+
return self._config
|
202
77
|
|
203
78
|
def is_enabled(self) -> bool:
|
204
79
|
"""Check if task system is enabled and properly configured."""
|
205
|
-
if not self.config:
|
206
|
-
return False
|
207
|
-
|
208
|
-
if not self.config.enabled:
|
80
|
+
if not self.config or not self.config.enabled:
|
209
81
|
return False
|
210
82
|
|
211
83
|
# Check if required dependencies are available
|
212
|
-
if dramatiq is None
|
213
|
-
logger.warning("Dramatiq
|
84
|
+
if dramatiq is None:
|
85
|
+
logger.warning("Dramatiq not available")
|
214
86
|
return False
|
215
87
|
|
216
88
|
return True
|
217
89
|
|
218
90
|
def get_redis_url(self) -> Optional[str]:
|
219
91
|
"""Get Redis URL from Django-CFG cache configuration."""
|
220
|
-
# Always try to get the URL if not cached
|
221
92
|
if self._redis_url is None:
|
222
93
|
try:
|
223
|
-
# Use get_current_config from django_cfg.core.config
|
224
94
|
from django_cfg.core.config import get_current_config
|
225
95
|
django_config = get_current_config()
|
226
96
|
|
227
|
-
# If that fails, try to import directly from api.config
|
228
97
|
if not django_config:
|
229
98
|
try:
|
230
99
|
from api.config import config
|
231
100
|
django_config = config
|
232
|
-
logger.debug("Got Django config from direct import")
|
233
101
|
except ImportError:
|
234
102
|
logger.warning("Could not import config from api.config")
|
235
103
|
|
236
|
-
logger.debug(f"Django config type: {type(django_config)}")
|
237
|
-
logger.debug(f"Has cache_default: {hasattr(django_config, 'cache_default') if django_config else False}")
|
238
|
-
|
239
104
|
if django_config and hasattr(django_config, 'cache_default') and django_config.cache_default:
|
240
105
|
cache_config = django_config.cache_default
|
241
|
-
logger.debug(f"Cache config type: {type(cache_config)}")
|
242
|
-
logger.debug(f"Cache config redis_url: {getattr(cache_config, 'redis_url', 'NOT_FOUND')}")
|
243
|
-
|
244
106
|
if hasattr(cache_config, 'redis_url') and cache_config.redis_url:
|
245
107
|
self._redis_url = cache_config.redis_url
|
246
108
|
logger.debug(f"Got Redis URL: {self._redis_url}")
|
247
109
|
elif hasattr(cache_config, 'location') and cache_config.location:
|
248
110
|
self._redis_url = cache_config.location
|
249
111
|
logger.debug(f"Got Redis URL from location: {self._redis_url}")
|
250
|
-
else:
|
251
|
-
logger.warning("Cache config exists but no redis_url or location found")
|
252
|
-
else:
|
253
|
-
logger.warning("No cache_default configuration found")
|
254
112
|
except Exception as e:
|
255
113
|
logger.warning(f"Failed to get Redis URL: {e}")
|
256
|
-
import traceback
|
257
|
-
logger.warning(f"Traceback: {traceback.format_exc()}")
|
258
114
|
|
259
115
|
return self._redis_url
|
260
116
|
|
261
|
-
def check_redis_connection(self) -> bool:
|
262
|
-
"""Check if Redis connection is available."""
|
263
|
-
redis_url = self.get_redis_url()
|
264
|
-
if not redis_url:
|
265
|
-
return False
|
266
|
-
|
267
|
-
if redis is None:
|
268
|
-
logger.error("Redis library not available")
|
269
|
-
return False
|
270
|
-
|
271
|
-
try:
|
272
|
-
parsed = urlparse(redis_url)
|
273
|
-
r = redis.Redis(
|
274
|
-
host=parsed.hostname or 'localhost',
|
275
|
-
port=parsed.port or 6379,
|
276
|
-
db=self.config.dramatiq.redis_db if self.config else 1,
|
277
|
-
password=parsed.password,
|
278
|
-
socket_timeout=5
|
279
|
-
)
|
280
|
-
|
281
|
-
# Test connection
|
282
|
-
r.ping()
|
283
|
-
return True
|
284
|
-
except Exception as e:
|
285
|
-
logger.error(f"Redis connection failed: {e}")
|
286
|
-
return False
|
287
|
-
|
288
117
|
def get_redis_client(self):
|
289
118
|
"""Get Redis client instance."""
|
290
119
|
redis_url = self.get_redis_url()
|
291
|
-
if not redis_url:
|
292
|
-
logger.warning("No Redis URL available for client")
|
293
|
-
return None
|
294
|
-
|
295
|
-
if redis is None:
|
296
|
-
logger.error("Redis library not available")
|
120
|
+
if not redis_url or redis is None:
|
297
121
|
return None
|
298
122
|
|
299
123
|
try:
|
300
124
|
parsed = urlparse(redis_url)
|
301
|
-
|
302
|
-
redis_db = 1 # default
|
303
|
-
try:
|
304
|
-
task_config = self.config # This should return TaskConfig
|
305
|
-
if task_config:
|
306
|
-
logger.debug(f"TaskConfig type: {type(task_config)}")
|
307
|
-
if hasattr(task_config, 'dramatiq') and task_config.dramatiq:
|
308
|
-
redis_db = task_config.dramatiq.redis_db
|
309
|
-
logger.debug(f"Using redis_db: {redis_db}")
|
310
|
-
else:
|
311
|
-
logger.warning("No dramatiq config found in TaskConfig")
|
312
|
-
else:
|
313
|
-
logger.warning("No TaskConfig available")
|
314
|
-
except Exception as e:
|
315
|
-
logger.error(f"Error getting redis_db: {e}")
|
316
|
-
|
317
|
-
client = redis.Redis(
|
125
|
+
return redis.Redis(
|
318
126
|
host=parsed.hostname or 'localhost',
|
319
127
|
port=parsed.port or 6379,
|
320
|
-
db=redis_db,
|
128
|
+
db=self.config.dramatiq.redis_db if self.config else 1,
|
321
129
|
password=parsed.password,
|
322
130
|
socket_timeout=5
|
323
131
|
)
|
324
|
-
|
325
|
-
logger.debug(f"Created Redis client: host={parsed.hostname}, port={parsed.port}, db={redis_db}")
|
326
|
-
return client
|
327
|
-
|
328
132
|
except Exception as e:
|
329
133
|
logger.error(f"Failed to create Redis client: {e}")
|
330
134
|
return None
|
331
135
|
|
332
|
-
def _get_current_timestamp(self) ->
|
333
|
-
"""Get current timestamp."""
|
334
|
-
|
136
|
+
def _get_current_timestamp(self) -> str:
|
137
|
+
"""Get current timestamp in ISO format."""
|
138
|
+
from datetime import datetime
|
139
|
+
return datetime.now().isoformat()
|
140
|
+
|
141
|
+
def check_redis_connection(self) -> bool:
|
142
|
+
"""Check if Redis connection is available."""
|
143
|
+
redis_client = self.get_redis_client()
|
144
|
+
if not redis_client:
|
145
|
+
return False
|
146
|
+
|
147
|
+
try:
|
148
|
+
redis_client.ping()
|
149
|
+
return True
|
150
|
+
except Exception as e:
|
151
|
+
logger.error(f"Redis connection failed: {e}")
|
152
|
+
return False
|
335
153
|
|
336
154
|
def validate_configuration(self) -> bool:
|
337
155
|
"""Validate complete task system configuration."""
|
@@ -346,38 +164,6 @@ class DjangoTasks(BaseModule):
|
|
346
164
|
|
347
165
|
return validate_task_config(self.config, redis_url)
|
348
166
|
|
349
|
-
def get_dramatiq_settings(self) -> Dict[str, Any]:
|
350
|
-
"""Generate Django settings for Dramatiq integration."""
|
351
|
-
if not self.config or not self.is_enabled():
|
352
|
-
return {}
|
353
|
-
|
354
|
-
redis_url = self.get_redis_url()
|
355
|
-
if not redis_url:
|
356
|
-
logger.error("Cannot generate Dramatiq settings: Redis URL not available")
|
357
|
-
return {}
|
358
|
-
|
359
|
-
try:
|
360
|
-
return self.config.get_dramatiq_settings(redis_url)
|
361
|
-
except Exception as e:
|
362
|
-
logger.error(f"Failed to generate Dramatiq settings: {e}")
|
363
|
-
return {}
|
364
|
-
|
365
|
-
|
366
|
-
def get_installed_apps(self) -> List[str]:
|
367
|
-
"""Get Django apps required for task system."""
|
368
|
-
if not self.is_enabled():
|
369
|
-
return []
|
370
|
-
|
371
|
-
apps = ["django_dramatiq"]
|
372
|
-
|
373
|
-
# Add optional apps based on configuration
|
374
|
-
if self.config and self.config.dramatiq.admin_enabled:
|
375
|
-
# Admin integration is included in django_dramatiq
|
376
|
-
# Add our custom tasks app for enhanced admin interface
|
377
|
-
apps.append("django_cfg.apps.tasks")
|
378
|
-
|
379
|
-
return apps
|
380
|
-
|
381
167
|
def discover_tasks(self) -> List[str]:
|
382
168
|
"""Discover task modules in Django apps."""
|
383
169
|
if not self.config or not self.config.auto_discover_tasks:
|
@@ -402,19 +188,13 @@ class DjangoTasks(BaseModule):
|
|
402
188
|
pass
|
403
189
|
except Exception as e:
|
404
190
|
logger.warning(f"Error importing task module {module_path}: {e}")
|
405
|
-
|
406
191
|
except Exception as e:
|
407
192
|
logger.error(f"Task discovery failed: {e}")
|
408
193
|
|
409
194
|
return discovered
|
410
195
|
|
411
196
|
def get_constance_fields(self) -> List[ConstanceField]:
|
412
|
-
"""
|
413
|
-
Get Constance fields for Dramatiq configuration.
|
414
|
-
|
415
|
-
Returns:
|
416
|
-
List of ConstanceField objects for dynamic task configuration
|
417
|
-
"""
|
197
|
+
"""Get Constance fields for Dramatiq configuration."""
|
418
198
|
if not self.is_enabled():
|
419
199
|
return []
|
420
200
|
|
@@ -460,84 +240,18 @@ class DjangoTasks(BaseModule):
|
|
460
240
|
logger.debug(f"Generated {len(fields)} Constance fields for Dramatiq")
|
461
241
|
return fields
|
462
242
|
|
463
|
-
def start_workers(self, processes: Optional[int] = None, queues: Optional[List[str]] = None) -> bool:
|
464
|
-
"""Start Dramatiq workers programmatically."""
|
465
|
-
logger.warning("Auto-start workers functionality has been removed. Please start workers manually using: python manage.py rundramatiq")
|
466
|
-
return False
|
467
|
-
|
468
|
-
def stop_workers(self, graceful: bool = True) -> bool:
|
469
|
-
"""Stop all Dramatiq workers."""
|
470
|
-
try:
|
471
|
-
timeout = self.config.dramatiq.worker.shutdown_timeout if self.config else 30
|
472
|
-
logger.info(f"Stopping workers (graceful={graceful}, timeout={timeout}s)")
|
473
|
-
|
474
|
-
# Find and kill Dramatiq worker processes
|
475
|
-
try:
|
476
|
-
# Find worker processes
|
477
|
-
result = subprocess.run(
|
478
|
-
["pgrep", "-f", "rundramatiq"],
|
479
|
-
capture_output=True,
|
480
|
-
text=True
|
481
|
-
)
|
482
|
-
|
483
|
-
if result.returncode == 0:
|
484
|
-
pids = result.stdout.strip().split('\n')
|
485
|
-
pids = [pid.strip() for pid in pids if pid.strip()]
|
486
|
-
|
487
|
-
if pids:
|
488
|
-
logger.info(f"Found {len(pids)} worker processes: {pids}")
|
489
|
-
|
490
|
-
# Send appropriate signal
|
491
|
-
signal = "TERM" if graceful else "KILL"
|
492
|
-
|
493
|
-
for pid in pids:
|
494
|
-
try:
|
495
|
-
subprocess.run(["kill", f"-{signal}", pid], check=True)
|
496
|
-
logger.info(f"Sent {signal} signal to worker process {pid}")
|
497
|
-
except subprocess.CalledProcessError:
|
498
|
-
logger.warning(f"Failed to send {signal} signal to process {pid}")
|
499
|
-
|
500
|
-
# Wait for graceful shutdown if requested
|
501
|
-
if graceful:
|
502
|
-
logger.info(f"Waiting up to {timeout}s for graceful shutdown...")
|
503
|
-
# TODO: Could implement actual waiting logic here
|
504
|
-
|
505
|
-
logger.info("✅ Dramatiq workers stopped successfully")
|
506
|
-
return True
|
507
|
-
else:
|
508
|
-
logger.info("No worker processes found")
|
509
|
-
return True
|
510
|
-
else:
|
511
|
-
logger.info("No worker processes found")
|
512
|
-
return True
|
513
|
-
|
514
|
-
except Exception as e:
|
515
|
-
logger.error(f"Failed to find/stop worker processes: {e}")
|
516
|
-
return False
|
517
|
-
|
518
|
-
except Exception as e:
|
519
|
-
logger.error(f"Failed to stop workers: {e}")
|
520
|
-
return False
|
521
|
-
|
522
243
|
def get_health_status(self) -> Dict[str, Any]:
|
523
244
|
"""Get comprehensive health status of task system."""
|
524
245
|
status = {
|
525
246
|
"enabled": self.is_enabled(),
|
526
247
|
"redis_connection": False,
|
527
248
|
"configuration_valid": False,
|
528
|
-
"workers": [],
|
529
|
-
"queues": [],
|
530
249
|
"discovered_modules": [],
|
531
250
|
}
|
532
251
|
|
533
252
|
if self.is_enabled():
|
534
253
|
status["redis_connection"] = self.check_redis_connection()
|
535
254
|
status["configuration_valid"] = self.validate_configuration()
|
536
|
-
|
537
|
-
if self.manager:
|
538
|
-
status["workers"] = self.manager.get_worker_stats()
|
539
|
-
status["queues"] = self.manager.get_queue_stats()
|
540
|
-
|
541
255
|
status["discovered_modules"] = self.discover_tasks()
|
542
256
|
|
543
257
|
return status
|
@@ -549,12 +263,7 @@ _task_service_instance: Optional[DjangoTasks] = None
|
|
549
263
|
|
550
264
|
|
551
265
|
def get_task_service() -> DjangoTasks:
|
552
|
-
"""
|
553
|
-
Get the global task service instance.
|
554
|
-
|
555
|
-
Returns:
|
556
|
-
DjangoTasks: The singleton task service instance
|
557
|
-
"""
|
266
|
+
"""Get the global task service instance."""
|
558
267
|
global _task_service_instance
|
559
268
|
|
560
269
|
if _task_service_instance is None:
|
@@ -594,79 +303,6 @@ def get_task_health() -> Dict[str, Any]:
|
|
594
303
|
}
|
595
304
|
|
596
305
|
|
597
|
-
def enqueue_task(actor_name: str, *args, queue_name: str = "default", **kwargs) -> bool:
|
598
|
-
"""
|
599
|
-
Enqueue a task for processing.
|
600
|
-
|
601
|
-
Args:
|
602
|
-
actor_name: Name of the Dramatiq actor
|
603
|
-
*args: Task arguments
|
604
|
-
queue_name: Queue to send task to
|
605
|
-
**kwargs: Task keyword arguments
|
606
|
-
|
607
|
-
Returns:
|
608
|
-
bool: True if task was successfully enqueued
|
609
|
-
"""
|
610
|
-
try:
|
611
|
-
service = get_task_service()
|
612
|
-
if not service.is_enabled():
|
613
|
-
logger.error("Task system not enabled")
|
614
|
-
return False
|
615
|
-
|
616
|
-
# TODO: Implement actual task enqueueing
|
617
|
-
# This would involve getting the actor and calling send()
|
618
|
-
|
619
|
-
logger.info(f"Enqueued task {actor_name} to queue {queue_name}")
|
620
|
-
return True
|
621
|
-
except Exception as e:
|
622
|
-
logger.error(f"Failed to enqueue task {actor_name}: {e}")
|
623
|
-
return False
|
624
|
-
|
625
|
-
|
626
|
-
def clear_dramatiq_queues() -> bool:
|
627
|
-
"""
|
628
|
-
Clear all Dramatiq queues on startup.
|
629
|
-
|
630
|
-
Returns:
|
631
|
-
bool: True if queues were cleared successfully
|
632
|
-
"""
|
633
|
-
try:
|
634
|
-
service = get_task_service()
|
635
|
-
if not service.is_enabled():
|
636
|
-
logger.debug("Task system not enabled, skipping queue clearing")
|
637
|
-
return True
|
638
|
-
|
639
|
-
# Get broker and clear all queues
|
640
|
-
if hasattr(service, 'manager') and service.manager and service.manager.broker:
|
641
|
-
broker = service.manager.broker
|
642
|
-
queue_names = service.config.dramatiq.queues
|
643
|
-
|
644
|
-
for queue_name in queue_names:
|
645
|
-
try:
|
646
|
-
# Clear the queue
|
647
|
-
if hasattr(broker, 'flush'):
|
648
|
-
broker.flush(queue_name)
|
649
|
-
logger.info(f"Cleared Dramatiq queue: {queue_name}")
|
650
|
-
elif hasattr(broker, 'client'):
|
651
|
-
# For Redis broker, clear using Redis client
|
652
|
-
redis_client = broker.client
|
653
|
-
queue_key = f"dramatiq:queue:{queue_name}"
|
654
|
-
redis_client.delete(queue_key)
|
655
|
-
logger.info(f"Cleared Redis queue: {queue_name}")
|
656
|
-
except Exception as e:
|
657
|
-
logger.warning(f"Failed to clear queue {queue_name}: {e}")
|
658
|
-
|
659
|
-
logger.info("✅ Dramatiq queues cleared on startup")
|
660
|
-
return True
|
661
|
-
else:
|
662
|
-
logger.debug("Broker not available, skipping queue clearing")
|
663
|
-
return True
|
664
|
-
|
665
|
-
except Exception as e:
|
666
|
-
logger.error(f"Failed to clear Dramatiq queues: {e}")
|
667
|
-
return False
|
668
|
-
|
669
|
-
|
670
306
|
def initialize_task_system():
|
671
307
|
"""
|
672
308
|
Initialize the task system during Django app startup.
|
@@ -682,14 +318,19 @@ def initialize_task_system():
|
|
682
318
|
if config and config.enabled:
|
683
319
|
logger.info("🔧 Initializing Django-CFG task system...")
|
684
320
|
|
685
|
-
#
|
321
|
+
# Set up Dramatiq broker from Django settings
|
686
322
|
try:
|
687
323
|
import dramatiq
|
688
|
-
|
689
|
-
|
690
|
-
|
324
|
+
from django.conf import settings
|
325
|
+
|
326
|
+
# Django-dramatiq automatically configures the broker from DRAMATIQ_BROKER setting
|
327
|
+
if hasattr(settings, 'DRAMATIQ_BROKER'):
|
328
|
+
logger.debug("✅ Dramatiq broker configured from Django settings")
|
329
|
+
else:
|
330
|
+
logger.warning("DRAMATIQ_BROKER not found in Django settings")
|
331
|
+
|
691
332
|
except Exception as e:
|
692
|
-
logger.warning(f"Failed to
|
333
|
+
logger.warning(f"Failed to configure Dramatiq: {e}")
|
693
334
|
|
694
335
|
logger.info("✅ Task system initialized successfully")
|
695
336
|
logger.info("💡 To start workers, run: python manage.py rundramatiq")
|
@@ -703,9 +344,6 @@ def initialize_task_system():
|
|
703
344
|
def extend_constance_config_with_tasks():
|
704
345
|
"""
|
705
346
|
Extend Constance configuration with Dramatiq task fields if tasks are enabled.
|
706
|
-
|
707
|
-
This function should be called during Django configuration setup to automatically
|
708
|
-
add task-related Constance fields when the task system is enabled.
|
709
347
|
"""
|
710
348
|
try:
|
711
349
|
service = get_task_service()
|
@@ -722,85 +360,14 @@ def extend_constance_config_with_tasks():
|
|
722
360
|
return []
|
723
361
|
|
724
362
|
|
725
|
-
# === Broker Creation ===
|
726
|
-
|
727
|
-
def create_dramatiq_broker():
|
728
|
-
"""
|
729
|
-
Create and configure Dramatiq broker from Django settings.
|
730
|
-
|
731
|
-
This function creates a broker instance that can be used directly
|
732
|
-
by the Dramatiq CLI without requiring a separate broker.py file.
|
733
|
-
|
734
|
-
Returns:
|
735
|
-
dramatiq.Broker: Configured Dramatiq broker instance
|
736
|
-
"""
|
737
|
-
try:
|
738
|
-
from django.conf import settings
|
739
|
-
|
740
|
-
if not hasattr(settings, 'DRAMATIQ_BROKER'):
|
741
|
-
raise RuntimeError("DRAMATIQ_BROKER not configured in Django settings")
|
742
|
-
|
743
|
-
broker_config = settings.DRAMATIQ_BROKER
|
744
|
-
|
745
|
-
# Create broker from Django settings
|
746
|
-
broker = RedisBroker(**broker_config['OPTIONS'])
|
747
|
-
|
748
|
-
# Add middleware (only if not already present)
|
749
|
-
existing_middleware_types = {type(mw).__name__ for mw in broker.middleware}
|
750
|
-
|
751
|
-
for middleware_path in broker_config['MIDDLEWARE']:
|
752
|
-
try:
|
753
|
-
module_path, class_name = middleware_path.rsplit('.', 1)
|
754
|
-
|
755
|
-
# Skip if middleware of this type already exists
|
756
|
-
if class_name in existing_middleware_types:
|
757
|
-
continue
|
758
|
-
|
759
|
-
module = __import__(module_path, fromlist=[class_name])
|
760
|
-
middleware_class = getattr(module, class_name)
|
761
|
-
broker.add_middleware(middleware_class())
|
762
|
-
except Exception as e:
|
763
|
-
logger.warning(f"Failed to add middleware {middleware_path}: {e}")
|
764
|
-
|
765
|
-
return broker
|
766
|
-
|
767
|
-
except Exception as e:
|
768
|
-
logger.error(f"Failed to create Dramatiq broker: {e}")
|
769
|
-
raise
|
770
|
-
|
771
|
-
|
772
|
-
# Global broker instance (lazy-loaded)
|
773
|
-
_broker_instance = None
|
774
|
-
|
775
|
-
|
776
|
-
def get_dramatiq_broker():
|
777
|
-
"""
|
778
|
-
Get the global Dramatiq broker instance.
|
779
|
-
|
780
|
-
Returns:
|
781
|
-
dramatiq.Broker: The singleton broker instance
|
782
|
-
"""
|
783
|
-
global _broker_instance
|
784
|
-
|
785
|
-
if _broker_instance is None:
|
786
|
-
_broker_instance = create_dramatiq_broker()
|
787
|
-
|
788
|
-
return _broker_instance
|
789
|
-
|
790
|
-
|
791
363
|
# === Exports ===
|
792
364
|
|
793
365
|
__all__ = [
|
794
366
|
"DjangoTasks",
|
795
|
-
"TaskManager",
|
796
367
|
"get_task_service",
|
797
368
|
"reset_task_service",
|
798
369
|
"is_task_system_available",
|
799
370
|
"get_task_health",
|
800
|
-
"enqueue_task",
|
801
371
|
"extend_constance_config_with_tasks",
|
802
372
|
"initialize_task_system",
|
803
|
-
|
804
|
-
"create_dramatiq_broker",
|
805
|
-
"get_dramatiq_broker",
|
806
|
-
]
|
373
|
+
]
|
@@ -0,0 +1,16 @@
|
|
1
|
+
"""
|
2
|
+
Dramatiq broker module for django-cfg CLI integration.
|
3
|
+
|
4
|
+
This module provides the broker instance required by Dramatiq CLI.
|
5
|
+
It's a thin wrapper around django_dramatiq.setup with broker export.
|
6
|
+
|
7
|
+
Usage:
|
8
|
+
dramatiq django_cfg.modules.dramatiq_setup [task_modules...]
|
9
|
+
"""
|
10
|
+
|
11
|
+
# Import django_dramatiq setup (handles Django initialization)
|
12
|
+
import django_dramatiq.setup
|
13
|
+
|
14
|
+
# Re-export the broker for Dramatiq CLI
|
15
|
+
import dramatiq
|
16
|
+
broker = dramatiq.get_broker()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-cfg
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.62
|
4
4
|
Summary: 🚀 Production-ready Django configuration framework with type-safe settings, smart automation, and modern developer experience
|
5
5
|
Project-URL: Homepage, https://github.com/markolofsen/django-cfg
|
6
6
|
Project-URL: Documentation, https://django-cfg.readthedocs.io
|
@@ -1,4 +1,4 @@
|
|
1
|
-
django_cfg/__init__.py,sha256=
|
1
|
+
django_cfg/__init__.py,sha256=hKbPbaVNg7IaLQ4vE3n3Y7U-mkn0685XzcnlerGqy-s,14288
|
2
2
|
django_cfg/apps.py,sha256=k84brkeXJI7EgKZLEpTkM9YFZofKI4PzhFOn1cl9Msc,1656
|
3
3
|
django_cfg/exceptions.py,sha256=RTQEoU3PfR8lqqNNv5ayd_HY2yJLs3eioqUy8VM6AG4,10378
|
4
4
|
django_cfg/integration.py,sha256=-7hvd-4ohLdzH4eufCZTOe3yTzPoQyB_HCfvsSm9AAw,5218
|
@@ -156,7 +156,7 @@ django_cfg/management/commands/create_token.py,sha256=beHtUTuyFZhG97F9vSkaX-u7ti
|
|
156
156
|
django_cfg/management/commands/generate.py,sha256=w0BF7IMftxNjxTxFuY8cw5pNKGW-LmTScJ8kZpxHu_8,4248
|
157
157
|
django_cfg/management/commands/list_urls.py,sha256=D8ikInA3uE1LbQGLWmfdLnEqPg7wqrI3caQA6iTe_-0,11009
|
158
158
|
django_cfg/management/commands/migrator.py,sha256=mhMM63uv_Jp9zHVVM7TMwCB90uv3iFZn1vOG-rXyi3s,16191
|
159
|
-
django_cfg/management/commands/rundramatiq.py,sha256=
|
159
|
+
django_cfg/management/commands/rundramatiq.py,sha256=1e07PWrtSnxTPZ8RCpgY_yxwV2y-EAduVZDZ34DRqoI,8896
|
160
160
|
django_cfg/management/commands/runserver_ngrok.py,sha256=mcTioDIzHgha6sGo5eazlJhdKr8y5-uEQIc3qG3AvCI,5237
|
161
161
|
django_cfg/management/commands/script.py,sha256=I6zOEQEGaED0HoLxl2EqKz39HwbKg9HhdxnGKybfH5s,16974
|
162
162
|
django_cfg/management/commands/show_config.py,sha256=0YJ99P1XvymT3fWritaNmn_HJ-PVb0I-yBy757M_bn8,8337
|
@@ -189,8 +189,9 @@ django_cfg/modules/base.py,sha256=X90X-0iBfnaUSaC7S8_ULa_rdT41tqTVJnT05_RuyK4,47
|
|
189
189
|
django_cfg/modules/django_email.py,sha256=uBvvqRVe1DG73Qq2d2IBYTjhFRdvHgsIbkVw3ge9OW8,16586
|
190
190
|
django_cfg/modules/django_logger.py,sha256=VfcPCurTdU3iI593EJNs3wUoWQowu7-ykJGuHNkE79M,6325
|
191
191
|
django_cfg/modules/django_ngrok.py,sha256=OAvir2pBFHfko-XaVgZTjeJwyZw-NSEILaKNlqQziqA,10476
|
192
|
-
django_cfg/modules/django_tasks.py,sha256=
|
192
|
+
django_cfg/modules/django_tasks.py,sha256=Vo45h4x8UHieAYdjp3g79efMns0cotkwp5ko4Fbt_pI,12868
|
193
193
|
django_cfg/modules/django_telegram.py,sha256=Mun2tAm0P2cUyQlAs8FaPe-FVgcrv7L_-FPTXQQEUT0,16356
|
194
|
+
django_cfg/modules/dramatiq_setup.py,sha256=Jke4aO_L1t6F3OAc4pl12zppKzL0gb1p6ilfQ3zUIZ8,454
|
194
195
|
django_cfg/modules/logger.py,sha256=4_zeasNehr8LGz8r_ckv15-fQS63zCodiqD4CYIEyFI,10546
|
195
196
|
django_cfg/modules/django_currency/README.md,sha256=Ox3jgRtsbOIaMuYDkIhrs9ijLGLbn-2R7mD9n2tjAVE,8512
|
196
197
|
django_cfg/modules/django_currency/__init__.py,sha256=SLzzYkkqoz9EsspkzEK0yZ4_Q3JKmb3e_c1GfdYF3GY,1294
|
@@ -251,8 +252,8 @@ django_cfg/templates/emails/base_email.html,sha256=TWcvYa2IHShlF_E8jf1bWZStRO0v8
|
|
251
252
|
django_cfg/utils/__init__.py,sha256=64wwXJuXytvwt8Ze_erSR2HmV07nGWJ6DV5wloRBvYE,435
|
252
253
|
django_cfg/utils/path_resolution.py,sha256=eML-6-RIGTs5TePktIQN8nxfDUEFJ3JA0AzWBcihAbs,13894
|
253
254
|
django_cfg/utils/smart_defaults.py,sha256=iL6_n3jGDW5812whylWAwXik0xBSYihdLp4IJ26T5eA,20547
|
254
|
-
django_cfg-1.1.
|
255
|
-
django_cfg-1.1.
|
256
|
-
django_cfg-1.1.
|
257
|
-
django_cfg-1.1.
|
258
|
-
django_cfg-1.1.
|
255
|
+
django_cfg-1.1.62.dist-info/METADATA,sha256=kN7aAQBvL6bjI5WJrhev0tknRjeiRBoNfFhtQscS74o,38953
|
256
|
+
django_cfg-1.1.62.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
257
|
+
django_cfg-1.1.62.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
|
258
|
+
django_cfg-1.1.62.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
|
259
|
+
django_cfg-1.1.62.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|