django-cfg 1.1.61__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.
Files changed (28) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/management/commands/rundramatiq.py +174 -202
  3. django_cfg/modules/django_tasks.py +54 -428
  4. django_cfg/modules/dramatiq_setup.py +16 -0
  5. {django_cfg-1.1.61.dist-info → django_cfg-1.1.62.dist-info}/METADATA +1 -1
  6. {django_cfg-1.1.61.dist-info → django_cfg-1.1.62.dist-info}/RECORD +9 -27
  7. django_cfg/apps/accounts/tests/__init__.py +0 -1
  8. django_cfg/apps/accounts/tests/test_models.py +0 -412
  9. django_cfg/apps/accounts/tests/test_otp_views.py +0 -143
  10. django_cfg/apps/accounts/tests/test_serializers.py +0 -331
  11. django_cfg/apps/accounts/tests/test_services.py +0 -401
  12. django_cfg/apps/accounts/tests/test_signals.py +0 -110
  13. django_cfg/apps/accounts/tests/test_views.py +0 -255
  14. django_cfg/apps/newsletter/tests/__init__.py +0 -1
  15. django_cfg/apps/newsletter/tests/run_tests.py +0 -47
  16. django_cfg/apps/newsletter/tests/test_email_integration.py +0 -256
  17. django_cfg/apps/newsletter/tests/test_email_tracking.py +0 -332
  18. django_cfg/apps/newsletter/tests/test_newsletter_manager.py +0 -83
  19. django_cfg/apps/newsletter/tests/test_newsletter_models.py +0 -157
  20. django_cfg/apps/support/tests/__init__.py +0 -0
  21. django_cfg/apps/support/tests/test_models.py +0 -106
  22. django_cfg/apps/tasks/@docs/CONFIGURATION.md +0 -663
  23. django_cfg/apps/tasks/@docs/README.md +0 -195
  24. django_cfg/apps/tasks/@docs/TASKS_QUEUES.md +0 -423
  25. django_cfg/apps/tasks/@docs/TROUBLESHOOTING.md +0 -506
  26. {django_cfg-1.1.61.dist-info → django_cfg-1.1.62.dist-info}/WHEEL +0 -0
  27. {django_cfg-1.1.61.dist-info → django_cfg-1.1.62.dist-info}/entry_points.txt +0 -0
  28. {django_cfg-1.1.61.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.60"
41
+ __version__ = "1.1.62"
42
42
  __author__ = "Unrealos Team"
43
43
  __email__ = "info@unrealos.com"
44
44
  __license__ = "MIT"
@@ -1,263 +1,235 @@
1
1
  """
2
2
  Django management command for running Dramatiq workers.
3
3
 
4
- This command provides a Django-integrated way to start Dramatiq workers
5
- with configuration from Django-CFG TaskConfig.
4
+ Based on django_dramatiq.management.commands.rundramatiq with Django-CFG integration.
5
+ Simple, clean, and working approach.
6
6
  """
7
7
 
8
- from django.core.management.base import BaseCommand, CommandError
9
- from django.conf import settings
10
- from typing import Any, Optional, List
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
- logger = logging.getLogger(__name__)
21
+
22
+ # Default values
23
+ NPROCS = multiprocessing.cpu_count()
24
+ NTHREADS = 8
18
25
 
19
26
 
20
27
  class Command(BaseCommand):
21
- """
22
- Run Dramatiq workers with Django-CFG configuration.
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
- """Add command line arguments."""
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="Number of worker processes (overrides config)",
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="Number of threads per process (overrides config)",
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="Comma-separated list of queues to process (overrides config)",
49
+ help="Listen to a subset of queues, or all when empty",
47
50
  )
48
51
  parser.add_argument(
49
- "--log-level",
50
- choices=["DEBUG", "INFO", "WARNING", "ERROR"],
51
- help="Log level for workers (overrides config)",
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
- "--watch",
57
+ "--pid-file",
55
58
  type=str,
56
- help="Watch directory for code changes and auto-reload",
59
+ help="Write the PID of the master process to this file",
57
60
  )
58
61
  parser.add_argument(
59
- "--pid-file",
62
+ "--log-file",
60
63
  type=str,
61
- help="Write process ID to file",
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, *args, **options):
70
- """Handle the command execution."""
71
- try:
72
- # Get task service
73
- task_service = get_task_service()
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
- # Check if task system is enabled
76
- if not task_service.is_enabled():
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
- # Validate configuration
83
- if not task_service.validate_configuration():
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
- # Get effective configuration
90
- config = task_service.config
91
- if not config:
92
- raise CommandError("Task configuration not available")
121
+ if queues:
122
+ process_args.extend(["--queues", *queues])
93
123
 
94
- # Build worker arguments
95
- worker_args = self._build_worker_args(config, options)
124
+ if pid_file:
125
+ process_args.extend(["--pid-file", pid_file])
96
126
 
97
- if options["dry_run"]:
98
- self._show_configuration(config, worker_args)
99
- return
127
+ if log_file:
128
+ process_args.extend(["--log-file", log_file])
100
129
 
101
- # Start workers
102
- self._start_workers(worker_args, options)
130
+ process_args.extend(tasks_modules)
103
131
 
104
- except ImportError:
105
- raise CommandError(
106
- "Dramatiq dependencies not installed. "
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)])
132
+ self.stdout.write(f"\nCommand that would be executed:")
133
+ self.stdout.write(f' {" ".join(process_args)}')
134
+ return
120
135
 
121
- # Thread count
122
- threads = options.get("threads") or config.dramatiq.threads
123
- args.extend(["--threads", str(threads)])
136
+ # Show startup info
137
+ self.stdout.write(self.style.SUCCESS("\nStarting Dramatiq workers..."))
124
138
 
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()
139
+ # Build dramatiq command
140
+ executable_name = "dramatiq"
141
+ executable_path = self._resolve_executable(executable_name)
130
142
 
131
- for queue in queues:
132
- args.extend(["--queues", queue])
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
+ ]
133
150
 
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])
151
+ # Add watch directory if specified
152
+ if watch_dir:
153
+ process_args.extend(["--watch", watch_dir])
137
154
 
138
- # Watch directory
139
- if options.get("watch"):
140
- args.extend(["--watch", options["watch"]])
155
+ # Add verbosity
156
+ verbosity_args = ["-v"] * (verbosity - 1)
157
+ process_args.extend(verbosity_args)
141
158
 
142
- # PID file
143
- if options.get("pid_file"):
144
- args.extend(["--pid-file", options["pid_file"]])
159
+ # Add queues if specified
160
+ if queues:
161
+ process_args.extend(["--queues", *queues])
145
162
 
146
- # Note: Using Python API instead of CLI, so no broker argument needed
163
+ # Add PID file if specified
164
+ if pid_file:
165
+ process_args.extend(["--pid-file", pid_file])
147
166
 
148
- # Add discovered task modules
149
- discovered_modules = get_task_service().discover_tasks()
150
- for module in discovered_modules:
151
- args.append(module)
167
+ # Add log file if specified
168
+ if log_file:
169
+ process_args.extend(["--log-file", log_file])
152
170
 
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")
171
+ # Add task modules (broker module first, then discovered modules)
172
+ process_args.extend(tasks_modules)
159
173
 
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
- )
174
+ self.stdout.write(f'Running dramatiq: "{" ".join(process_args)}"\n')
167
175
 
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)")
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))
181
197
 
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 subprocess like django_dramatiq does."""
187
- self.stdout.write(
188
- self.style.SUCCESS("Starting Dramatiq workers...")
189
- )
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"]
190
204
 
191
- # Show startup info
205
+ # Get task service for configuration
192
206
  task_service = get_task_service()
193
- config = task_service.config
194
-
195
- processes = options.get("processes") or config.get_effective_processes()
196
- threads = options.get("threads") or config.dramatiq.threads
197
- queues = config.get_effective_queues()
198
- if options.get("queues"):
199
- queues = [q.strip() for q in options["queues"].split(",")]
200
-
201
- self.stdout.write(f"Processes: {processes}")
202
- self.stdout.write(f"Threads: {threads}")
203
- self.stdout.write(f"Queues: {', '.join(queues)}")
204
207
 
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']}")
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)
210
214
 
211
- try:
212
- # Build process arguments like django_dramatiq does
213
- executable_name = "dramatiq"
214
- executable_path = self._resolve_executable(executable_name)
215
-
216
- # Discover task modules (including django_dramatiq.setup)
217
- discovered_modules = self._discover_task_modules()
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",))
218
218
 
219
- process_args = [
220
- executable_name,
221
- "--processes", str(processes),
222
- "--threads", str(threads),
223
- ]
224
-
225
- # Add queues
226
- if queues:
227
- process_args.extend(["--queues"] + queues)
228
-
229
- # Add task modules (including django_dramatiq.setup)
230
- process_args.extend(discovered_modules)
231
-
232
- self.stdout.write(f"Running dramatiq: {' '.join(process_args)}")
233
-
234
- # Use subprocess like django_dramatiq does
235
- import subprocess
236
- if sys.platform == "win32":
237
- command = [executable_path] + process_args[1:]
238
- sys.exit(subprocess.run(command))
239
- else:
240
- # Use os.execvp like django_dramatiq
241
- os.execvp(executable_path, process_args)
242
-
243
- except KeyboardInterrupt:
244
- self.stdout.write("\nShutting down workers...")
245
- except Exception as e:
246
- logger.exception("Worker execution failed")
247
- raise CommandError(f"Worker execution failed: {e}")
248
-
249
- def _discover_task_modules(self):
250
- """Discover task modules like django_dramatiq does."""
251
- task_modules = ["django_dramatiq.setup"] # Always include setup module
252
-
253
- # Add discovered modules from django-cfg
254
- discovered_modules = get_task_service().discover_tasks()
255
- for module_name in discovered_modules:
256
- self.stdout.write(f"Discovered tasks module: {module_name}")
257
- task_modules.append(module_name)
258
-
259
- return task_modules
260
-
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
+
261
233
  def _resolve_executable(self, exec_name):
262
234
  """Resolve executable path like django_dramatiq does."""
263
235
  bin_dir = os.path.dirname(sys.executable)
@@ -266,4 +238,4 @@ class Command(BaseCommand):
266
238
  exec_path = os.path.join(d, exec_name)
267
239
  if os.path.isfile(exec_path):
268
240
  return exec_path
269
- return exec_name
241
+ return exec_name