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 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.59"
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
- 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)])
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
- 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(",")]
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
- self.stdout.write(f"Processes: {processes}")
202
- self.stdout.write(f"Threads: {threads}")
203
- self.stdout.write(f"Queues: {', '.join(queues)}")
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
- # CRITICAL: Set the broker as the global default BEFORE importing tasks
221
- dramatiq.set_broker(broker)
222
-
223
- # Now discover and import task modules with the correct broker set
224
- discovered_modules = get_task_service().discover_tasks()
225
- for module_name in discovered_modules:
226
- try:
227
- __import__(module_name)
228
- self.stdout.write(f"Loaded tasks from: {module_name}")
229
- except ImportError as e:
230
- self.stdout.write(f"Warning: Could not import {module_name}: {e}")
231
-
232
- # Verify broker is set correctly
233
- current_broker = dramatiq.get_broker()
234
- self.stdout.write(f"Using broker: {current_broker}")
235
-
236
- # Create and start worker
237
- worker = Worker(broker, worker_timeout=600000)
238
- worker.start()
239
-
240
- except KeyboardInterrupt:
241
- self.stdout.write("\nShutting down workers...")
242
- except Exception as e:
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
- This module provides the service layer for background task processing with Dramatiq,
5
- including task management, worker control, monitoring, and Django integration.
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, Union
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, DramatiqConfig, validate_task_config
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
- Main Django-CFG task service.
40
+ Simplified Django-CFG task service.
142
41
 
143
- Provides the primary interface for task system integration,
144
- configuration management, and service coordination.
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
- # Always try to get fresh config to avoid stale cache issues
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
- self._manager = TaskManager(self.config.dramatiq)
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.error(f"Failed to create task manager: {e}")
200
- return None
201
- return self._manager
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 or django_dramatiq is None:
213
- logger.warning("Dramatiq dependencies not available")
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
- # Get redis_db from config, with fallback
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) -> int:
333
- """Get current timestamp."""
334
- return int(time.time())
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
- # CRITICAL: Set up global broker for task sending
321
+ # Set up Dramatiq broker from Django settings
686
322
  try:
687
323
  import dramatiq
688
- broker = get_dramatiq_broker()
689
- dramatiq.set_broker(broker)
690
- logger.debug(f"Set global Dramatiq broker: {broker}")
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 set global broker: {e}")
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
- "clear_dramatiq_queues",
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.59
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=9_vseCVwoJ7CXG8_EjpsdPmqrw021qouoH-wvSSGNwA,14288
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=y9LYzko8ppZw_nQZPIIQ3v5-gqxFz6U0Yx35eQzc5Ag,9117
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=qulUuE76sgTykHLaBp6k9A29ULHnp-lUNdsZfNBFZC0,29048
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.59.dist-info/METADATA,sha256=fhstTU5UCjrxQMKM692CCMivv-FNyiZleZBBPkQTyKw,38953
255
- django_cfg-1.1.59.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
256
- django_cfg-1.1.59.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
257
- django_cfg-1.1.59.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
258
- django_cfg-1.1.59.dist-info/RECORD,,
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,,