django-cfg 1.4.9__py3-none-any.whl → 1.4.11__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/apps/agents/management/commands/create_agent.py +1 -1
- django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
- django_cfg/apps/newsletter/serializers.py +40 -3
- django_cfg/apps/newsletter/views/campaigns.py +12 -3
- django_cfg/apps/newsletter/views/emails.py +14 -3
- django_cfg/apps/newsletter/views/subscriptions.py +12 -2
- django_cfg/apps/payments/middleware/api_access.py +6 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +2 -1
- django_cfg/apps/payments/middleware/usage_tracking.py +5 -1
- django_cfg/apps/payments/models/managers/api_key_managers.py +0 -1
- django_cfg/apps/payments/models/managers/subscription_managers.py +0 -1
- django_cfg/apps/payments/services/core/balance_service.py +5 -5
- django_cfg/apps/payments/services/core/subscription_service.py +1 -2
- django_cfg/apps/payments/views/api/balances.py +8 -7
- django_cfg/apps/payments/views/api/base.py +10 -6
- django_cfg/apps/payments/views/api/currencies.py +53 -10
- django_cfg/apps/payments/views/api/payments.py +3 -1
- django_cfg/apps/payments/views/api/subscriptions.py +2 -5
- django_cfg/apps/payments/views/api/webhooks.py +72 -7
- django_cfg/apps/payments/views/overview/serializers.py +34 -1
- django_cfg/apps/payments/views/overview/views.py +2 -1
- django_cfg/apps/payments/views/serializers/payments.py +6 -6
- django_cfg/apps/urls.py +106 -45
- django_cfg/core/base/config_model.py +2 -2
- django_cfg/core/constants.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/api.py +82 -41
- django_cfg/core/integration/display/startup.py +30 -22
- django_cfg/core/integration/url_integration.py +15 -16
- django_cfg/dashboard/sections/documentation.py +391 -0
- django_cfg/management/commands/check_endpoints.py +11 -160
- django_cfg/management/commands/check_settings.py +13 -265
- django_cfg/management/commands/clear_constance.py +13 -201
- django_cfg/management/commands/create_token.py +13 -321
- django_cfg/management/commands/generate_clients.py +23 -0
- django_cfg/management/commands/list_urls.py +13 -306
- django_cfg/management/commands/migrate_all.py +13 -126
- django_cfg/management/commands/migrator.py +13 -396
- django_cfg/management/commands/rundramatiq.py +15 -247
- django_cfg/management/commands/rundramatiq_simulator.py +12 -429
- django_cfg/management/commands/runserver_ngrok.py +15 -160
- django_cfg/management/commands/script.py +12 -488
- django_cfg/management/commands/show_config.py +12 -215
- django_cfg/management/commands/show_urls.py +12 -342
- django_cfg/management/commands/superuser.py +15 -295
- django_cfg/management/commands/task_clear.py +14 -217
- django_cfg/management/commands/task_status.py +13 -248
- django_cfg/management/commands/test_email.py +15 -86
- django_cfg/management/commands/test_telegram.py +14 -61
- django_cfg/management/commands/test_twilio.py +15 -105
- django_cfg/management/commands/tree.py +13 -383
- django_cfg/management/commands/validate_openapi.py +10 -0
- django_cfg/middleware/README.md +1 -1
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/__init__.py +2 -2
- django_cfg/models/api/drf/spectacular.py +6 -6
- django_cfg/models/django/__init__.py +2 -2
- django_cfg/models/django/openapi.py +238 -0
- django_cfg/models/django/{revolution.py → revolution_legacy.py} +8 -0
- django_cfg/modules/django_admin/management/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
- django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
- django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
- django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
- django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
- django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
- django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
- django_cfg/modules/django_admin/management/commands/script.py +496 -0
- django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
- django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
- django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
- django_cfg/modules/django_admin/management/commands/tree.py +390 -0
- django_cfg/modules/django_client/__init__.py +20 -0
- django_cfg/modules/django_client/apps.py +35 -0
- django_cfg/modules/django_client/core/__init__.py +56 -0
- django_cfg/modules/django_client/core/archive/__init__.py +11 -0
- django_cfg/modules/django_client/core/archive/manager.py +134 -0
- django_cfg/modules/django_client/core/cli/__init__.py +12 -0
- django_cfg/modules/django_client/core/cli/main.py +235 -0
- django_cfg/modules/django_client/core/config/__init__.py +18 -0
- django_cfg/modules/django_client/core/config/config.py +188 -0
- django_cfg/modules/django_client/core/config/group.py +101 -0
- django_cfg/modules/django_client/core/config/service.py +209 -0
- django_cfg/modules/django_client/core/generator/__init__.py +115 -0
- django_cfg/modules/django_client/core/generator/base.py +767 -0
- django_cfg/modules/django_client/core/generator/python.py +751 -0
- django_cfg/modules/django_client/core/generator/templates/python/__init__.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/python/api_wrapper.py.jinja +130 -0
- django_cfg/modules/django_client/core/generator/templates/python/app_init.py.jinja +6 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/app_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/flat_client.py.jinja +38 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/operation_method.py.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/sub_client.py.jinja +11 -0
- django_cfg/modules/django_client/core/generator/templates/python/client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/main_init.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/app_models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enum_class.py.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enums.py.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/schema_class.py.jinja +19 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/logger.py.jinja +255 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/schema.py.jinja +12 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/app_index.ts.jinja +2 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/app_client.ts.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/client.ts.jinja +327 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/flat_client.ts.jinja +109 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/main_client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/operation.ts.jinja +61 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/sub_client.ts.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/index.ts.jinja +5 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/main_index.ts.jinja +206 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/app_models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/enums.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/errors.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/http.ts.jinja +98 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/logger.ts.jinja +251 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/schema.ts.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/storage.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/typescript.py +872 -0
- django_cfg/modules/django_client/core/groups/__init__.py +13 -0
- django_cfg/modules/django_client/core/groups/detector.py +178 -0
- django_cfg/modules/django_client/core/groups/manager.py +314 -0
- django_cfg/modules/django_client/core/ir/__init__.py +57 -0
- django_cfg/modules/django_client/core/ir/context.py +387 -0
- django_cfg/modules/django_client/core/ir/operation.py +518 -0
- django_cfg/modules/django_client/core/ir/schema.py +353 -0
- django_cfg/modules/django_client/core/parser/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/base.py +648 -0
- django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/models/base.py +212 -0
- django_cfg/modules/django_client/core/parser/models/components.py +160 -0
- django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
- django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
- django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
- django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
- django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
- django_cfg/modules/django_client/core/validation/__init__.py +22 -0
- django_cfg/modules/django_client/core/validation/checker.py +134 -0
- django_cfg/modules/django_client/core/validation/fixer.py +216 -0
- django_cfg/modules/django_client/core/validation/reporter.py +480 -0
- django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
- django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
- django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
- django_cfg/modules/django_client/core/validation/safety.py +266 -0
- django_cfg/modules/django_client/management/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +422 -0
- django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
- django_cfg/modules/django_client/spectacular/__init__.py +9 -0
- django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
- django_cfg/modules/django_client/urls.py +72 -0
- django_cfg/modules/django_email/management/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/test_email.py +93 -0
- django_cfg/modules/django_logging/django_logger.py +6 -6
- django_cfg/modules/django_ngrok/management/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
- django_cfg/modules/django_tasks/management/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
- django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
- django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
- django_cfg/modules/django_telegram/management/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
- django_cfg/modules/django_twilio/management/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
- django_cfg/modules/django_unfold/callbacks/main.py +16 -5
- django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
- django_cfg/modules/django_unfold/dashboard.py +1 -1
- django_cfg/pyproject.toml +2 -6
- django_cfg/registry/third_party.py +5 -7
- django_cfg/routing/callbacks.py +1 -1
- django_cfg/static/admin/css/prose-unfold.css +666 -0
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/index_new.html +13 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
- django_cfg/templates/admin/sections/documentation_section.html +172 -0
- django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/RECORD +192 -71
- django_cfg/management/commands/generate.py +0 -107
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,437 @@
|
|
1
|
+
"""
|
2
|
+
Django management command for simulating Dramatiq tasks and workers.
|
3
|
+
|
4
|
+
Usage:
|
5
|
+
python manage.py rundramatiq_simulator --help
|
6
|
+
python manage.py rundramatiq_simulator --workers 5
|
7
|
+
python manage.py rundramatiq_simulator --clear-only
|
8
|
+
python manage.py rundramatiq_simulator --show-keys
|
9
|
+
"""
|
10
|
+
|
11
|
+
import redis
|
12
|
+
import json
|
13
|
+
import time
|
14
|
+
from datetime import datetime, timezone
|
15
|
+
import random
|
16
|
+
from typing import Dict, Any, Optional
|
17
|
+
|
18
|
+
from django.core.management.base import BaseCommand, CommandError
|
19
|
+
from django.conf import settings
|
20
|
+
|
21
|
+
from django_cfg.modules.django_tasks import DjangoTasks
|
22
|
+
|
23
|
+
|
24
|
+
class TaskSimulator:
|
25
|
+
"""Task data simulator for Tasks Dashboard."""
|
26
|
+
|
27
|
+
def __init__(self):
|
28
|
+
"""Initialize the simulator."""
|
29
|
+
self.tasks_service = DjangoTasks()
|
30
|
+
|
31
|
+
# Get Redis client using the same logic as DjangoTasks
|
32
|
+
try:
|
33
|
+
redis_url = self.tasks_service.get_redis_url()
|
34
|
+
if not redis_url:
|
35
|
+
raise RuntimeError("No Redis URL available")
|
36
|
+
|
37
|
+
# Parse URL for connection
|
38
|
+
from urllib.parse import urlparse
|
39
|
+
parsed = urlparse(redis_url)
|
40
|
+
|
41
|
+
self.redis_client = redis.Redis(
|
42
|
+
host=parsed.hostname or 'localhost',
|
43
|
+
port=parsed.port or 6379,
|
44
|
+
db=int(parsed.path.lstrip('/')) if parsed.path else 1,
|
45
|
+
decode_responses=True
|
46
|
+
)
|
47
|
+
|
48
|
+
except Exception as e:
|
49
|
+
raise CommandError(f"Failed to connect to Redis: {e}")
|
50
|
+
|
51
|
+
# Get queue configuration
|
52
|
+
try:
|
53
|
+
config = self.tasks_service.get_config()
|
54
|
+
self.queues = config.tasks.dramatiq.queues
|
55
|
+
except Exception as e:
|
56
|
+
# Use default queues if we can't get configuration
|
57
|
+
self.queues = ['critical', 'high', 'default', 'low', 'background', 'payments', 'agents', 'knowbase']
|
58
|
+
|
59
|
+
def clear_all_data(self) -> int:
|
60
|
+
"""
|
61
|
+
Clear all test data.
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
Number of deleted keys
|
65
|
+
"""
|
66
|
+
keys = self.redis_client.keys("dramatiq:*")
|
67
|
+
if keys:
|
68
|
+
deleted = self.redis_client.delete(*keys)
|
69
|
+
return deleted
|
70
|
+
return 0
|
71
|
+
|
72
|
+
def simulate_queues(self, pending_tasks_per_queue=None, failed_tasks_per_queue=None) -> Dict[str, Dict[str, int]]:
|
73
|
+
"""
|
74
|
+
Simulate queues with tasks.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
pending_tasks_per_queue: Dict[str, int] - number of pending tasks per queue
|
78
|
+
failed_tasks_per_queue: Dict[str, int] - number of failed tasks per queue
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
Dict with information about created tasks
|
82
|
+
"""
|
83
|
+
if pending_tasks_per_queue is None:
|
84
|
+
pending_tasks_per_queue = {
|
85
|
+
'critical': 2,
|
86
|
+
'high': 5,
|
87
|
+
'default': 12,
|
88
|
+
'low': 8,
|
89
|
+
'background': 15,
|
90
|
+
'payments': 3,
|
91
|
+
'agents': 7,
|
92
|
+
'knowbase': 4
|
93
|
+
}
|
94
|
+
|
95
|
+
if failed_tasks_per_queue is None:
|
96
|
+
failed_tasks_per_queue = {
|
97
|
+
'critical': 0,
|
98
|
+
'high': 1,
|
99
|
+
'default': 3,
|
100
|
+
'low': 2,
|
101
|
+
'background': 1,
|
102
|
+
'payments': 0,
|
103
|
+
'agents': 1,
|
104
|
+
'knowbase': 0
|
105
|
+
}
|
106
|
+
|
107
|
+
results = {}
|
108
|
+
|
109
|
+
for queue_name in self.queues:
|
110
|
+
queue_results = {'pending': 0, 'failed': 0}
|
111
|
+
|
112
|
+
# Pending tasks
|
113
|
+
pending_count = pending_tasks_per_queue.get(queue_name, 0)
|
114
|
+
if pending_count > 0:
|
115
|
+
queue_key = f"dramatiq:default.DQ.{queue_name}"
|
116
|
+
|
117
|
+
# Add fake tasks to queue
|
118
|
+
for i in range(pending_count):
|
119
|
+
task_data = {
|
120
|
+
"queue_name": queue_name,
|
121
|
+
"actor_name": f"process_{queue_name}_task",
|
122
|
+
"args": [f"task_{i}"],
|
123
|
+
"kwargs": {},
|
124
|
+
"options": {},
|
125
|
+
"message_id": f"msg_{queue_name}_{i}_{int(time.time())}",
|
126
|
+
"message_timestamp": int(time.time() * 1000)
|
127
|
+
}
|
128
|
+
self.redis_client.lpush(queue_key, json.dumps(task_data))
|
129
|
+
|
130
|
+
queue_results['pending'] = pending_count
|
131
|
+
|
132
|
+
# Failed tasks
|
133
|
+
failed_count = failed_tasks_per_queue.get(queue_name, 0)
|
134
|
+
if failed_count > 0:
|
135
|
+
failed_key = f"dramatiq:default.DQ.{queue_name}.failed"
|
136
|
+
|
137
|
+
# Add fake failed tasks
|
138
|
+
for i in range(failed_count):
|
139
|
+
failed_task_data = {
|
140
|
+
"queue_name": queue_name,
|
141
|
+
"actor_name": f"failed_{queue_name}_task",
|
142
|
+
"args": [f"failed_task_{i}"],
|
143
|
+
"kwargs": {},
|
144
|
+
"options": {},
|
145
|
+
"message_id": f"failed_msg_{queue_name}_{i}_{int(time.time())}",
|
146
|
+
"message_timestamp": int(time.time() * 1000),
|
147
|
+
"error": f"Simulated error for {queue_name} task {i}"
|
148
|
+
}
|
149
|
+
self.redis_client.lpush(failed_key, json.dumps(failed_task_data))
|
150
|
+
|
151
|
+
queue_results['failed'] = failed_count
|
152
|
+
|
153
|
+
if queue_results['pending'] > 0 or queue_results['failed'] > 0:
|
154
|
+
results[queue_name] = queue_results
|
155
|
+
|
156
|
+
return results
|
157
|
+
|
158
|
+
def simulate_workers(self, worker_count=3) -> list:
|
159
|
+
"""
|
160
|
+
Симулировать активных воркеров.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
worker_count: Количество воркеров для симуляции
|
164
|
+
|
165
|
+
Returns:
|
166
|
+
Список ID созданных воркеров
|
167
|
+
"""
|
168
|
+
worker_ids = []
|
169
|
+
|
170
|
+
for i in range(worker_count):
|
171
|
+
worker_id = f"worker_{i}_{int(time.time())}"
|
172
|
+
worker_key = f"dramatiq:worker:{worker_id}"
|
173
|
+
|
174
|
+
worker_data = {
|
175
|
+
"worker_id": worker_id,
|
176
|
+
"hostname": f"localhost",
|
177
|
+
"pid": 1000 + i,
|
178
|
+
"queues": self.queues,
|
179
|
+
"started_at": datetime.now(timezone.utc).isoformat(),
|
180
|
+
"last_heartbeat": datetime.now(timezone.utc).isoformat(),
|
181
|
+
"status": "active"
|
182
|
+
}
|
183
|
+
|
184
|
+
# Устанавливаем данные воркера с TTL
|
185
|
+
self.redis_client.setex(
|
186
|
+
worker_key,
|
187
|
+
300, # 5 минут TTL
|
188
|
+
json.dumps(worker_data)
|
189
|
+
)
|
190
|
+
|
191
|
+
worker_ids.append(worker_id)
|
192
|
+
|
193
|
+
return worker_ids
|
194
|
+
|
195
|
+
def simulate_task_statistics(self) -> Dict[str, Any]:
|
196
|
+
"""
|
197
|
+
Симулировать статистику задач.
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
Созданная статистика
|
201
|
+
"""
|
202
|
+
stats_data = {
|
203
|
+
"total_processed": random.randint(1000, 2000),
|
204
|
+
"total_failed": random.randint(30, 80),
|
205
|
+
"total_retried": random.randint(15, 40),
|
206
|
+
"processing_time_avg": round(random.uniform(1.5, 4.0), 2),
|
207
|
+
"last_updated": datetime.now(timezone.utc).isoformat()
|
208
|
+
}
|
209
|
+
|
210
|
+
stats_key = "dramatiq:stats"
|
211
|
+
self.redis_client.setex(stats_key, 3600, json.dumps(stats_data))
|
212
|
+
|
213
|
+
return stats_data
|
214
|
+
|
215
|
+
def run_simulation(self, workers=3, clear_first=True) -> Dict[str, Any]:
|
216
|
+
"""
|
217
|
+
Запустить полную симуляцию.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
workers: Количество воркеров
|
221
|
+
clear_first: Очистить данные перед симуляцией
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
Результаты симуляции
|
225
|
+
"""
|
226
|
+
results = {
|
227
|
+
'cleared_keys': 0,
|
228
|
+
'queues': {},
|
229
|
+
'workers': [],
|
230
|
+
'statistics': {}
|
231
|
+
}
|
232
|
+
|
233
|
+
if clear_first:
|
234
|
+
results['cleared_keys'] = self.clear_all_data()
|
235
|
+
|
236
|
+
results['queues'] = self.simulate_queues()
|
237
|
+
results['workers'] = self.simulate_workers(workers)
|
238
|
+
results['statistics'] = self.simulate_task_statistics()
|
239
|
+
|
240
|
+
return results
|
241
|
+
|
242
|
+
def get_redis_summary(self) -> Dict[str, Any]:
|
243
|
+
"""Получить сводку по данным в Redis."""
|
244
|
+
summary = {
|
245
|
+
'total_keys': 0,
|
246
|
+
'queues': {},
|
247
|
+
'workers': 0,
|
248
|
+
'statistics': None
|
249
|
+
}
|
250
|
+
|
251
|
+
# Подсчитываем все ключи
|
252
|
+
all_keys = self.redis_client.keys("dramatiq:*")
|
253
|
+
summary['total_keys'] = len(all_keys)
|
254
|
+
|
255
|
+
# Анализируем очереди
|
256
|
+
for queue_name in self.queues:
|
257
|
+
pending_key = f"dramatiq:default.DQ.{queue_name}"
|
258
|
+
failed_key = f"dramatiq:default.DQ.{queue_name}.failed"
|
259
|
+
|
260
|
+
pending = self.redis_client.llen(pending_key)
|
261
|
+
failed = self.redis_client.llen(failed_key)
|
262
|
+
|
263
|
+
if pending > 0 or failed > 0:
|
264
|
+
summary['queues'][queue_name] = {
|
265
|
+
'pending': pending,
|
266
|
+
'failed': failed
|
267
|
+
}
|
268
|
+
|
269
|
+
# Подсчитываем воркеров
|
270
|
+
worker_keys = self.redis_client.keys("dramatiq:worker:*")
|
271
|
+
summary['workers'] = len(worker_keys)
|
272
|
+
|
273
|
+
# Получаем статистику
|
274
|
+
stats_key = "dramatiq:stats"
|
275
|
+
if self.redis_client.exists(stats_key):
|
276
|
+
try:
|
277
|
+
stats_data = self.redis_client.get(stats_key)
|
278
|
+
summary['statistics'] = json.loads(stats_data)
|
279
|
+
except:
|
280
|
+
pass
|
281
|
+
|
282
|
+
return summary
|
283
|
+
|
284
|
+
|
285
|
+
class Command(BaseCommand):
|
286
|
+
"""Django management command для симуляции Dramatiq данных."""
|
287
|
+
|
288
|
+
# Web execution metadata
|
289
|
+
web_executable = False
|
290
|
+
requires_input = False
|
291
|
+
is_destructive = False
|
292
|
+
|
293
|
+
help = 'Simulate Dramatiq tasks and workers for dashboard testing'
|
294
|
+
|
295
|
+
def add_arguments(self, parser):
|
296
|
+
"""Добавить аргументы команды."""
|
297
|
+
parser.add_argument(
|
298
|
+
'--workers',
|
299
|
+
type=int,
|
300
|
+
default=3,
|
301
|
+
help='Number of workers to simulate (default: 3)'
|
302
|
+
)
|
303
|
+
|
304
|
+
parser.add_argument(
|
305
|
+
'--no-clear',
|
306
|
+
action='store_true',
|
307
|
+
help='Do not clear existing data before simulation'
|
308
|
+
)
|
309
|
+
|
310
|
+
parser.add_argument(
|
311
|
+
'--clear-only',
|
312
|
+
action='store_true',
|
313
|
+
help='Only clear data, do not simulate'
|
314
|
+
)
|
315
|
+
|
316
|
+
parser.add_argument(
|
317
|
+
'--show-keys',
|
318
|
+
action='store_true',
|
319
|
+
help='Show Redis keys after operation'
|
320
|
+
)
|
321
|
+
|
322
|
+
parser.add_argument(
|
323
|
+
'--summary',
|
324
|
+
action='store_true',
|
325
|
+
help='Show summary of current Redis data'
|
326
|
+
)
|
327
|
+
|
328
|
+
def handle(self, *args, **options):
|
329
|
+
"""Выполнить команду."""
|
330
|
+
try:
|
331
|
+
simulator = TaskSimulator()
|
332
|
+
|
333
|
+
# Показать только сводку
|
334
|
+
if options['summary']:
|
335
|
+
self.show_summary(simulator)
|
336
|
+
return
|
337
|
+
|
338
|
+
# Только очистка
|
339
|
+
if options['clear_only']:
|
340
|
+
self.stdout.write("🧹 Clearing all test data...")
|
341
|
+
cleared = simulator.clear_all_data()
|
342
|
+
self.stdout.write(
|
343
|
+
self.style.SUCCESS(f"✅ Cleared {cleared} Redis keys")
|
344
|
+
)
|
345
|
+
return
|
346
|
+
|
347
|
+
# Полная симуляция
|
348
|
+
self.stdout.write("🎭 Starting Dramatiq Task Simulation")
|
349
|
+
self.stdout.write("=" * 50)
|
350
|
+
|
351
|
+
results = simulator.run_simulation(
|
352
|
+
workers=options['workers'],
|
353
|
+
clear_first=not options['no_clear']
|
354
|
+
)
|
355
|
+
|
356
|
+
# Показываем результаты
|
357
|
+
if results['cleared_keys'] > 0:
|
358
|
+
self.stdout.write(f"🧹 Cleared {results['cleared_keys']} existing keys")
|
359
|
+
|
360
|
+
self.stdout.write(f"📋 Created queues:")
|
361
|
+
total_pending = 0
|
362
|
+
total_failed = 0
|
363
|
+
|
364
|
+
for queue_name, counts in results['queues'].items():
|
365
|
+
pending = counts['pending']
|
366
|
+
failed = counts['failed']
|
367
|
+
total_pending += pending
|
368
|
+
total_failed += failed
|
369
|
+
|
370
|
+
self.stdout.write(f" {queue_name}: {pending} pending, {failed} failed")
|
371
|
+
|
372
|
+
self.stdout.write(f"👷 Created {len(results['workers'])} workers")
|
373
|
+
self.stdout.write(f"📊 Added task statistics")
|
374
|
+
|
375
|
+
self.stdout.write("=" * 50)
|
376
|
+
self.stdout.write(self.style.SUCCESS("✅ Simulation completed!"))
|
377
|
+
|
378
|
+
self.stdout.write(f"\n📊 Summary:")
|
379
|
+
active_queues = len(results['queues'])
|
380
|
+
self.stdout.write(f" Active Queues: {active_queues}")
|
381
|
+
self.stdout.write(f" Active Workers: {len(results['workers'])}")
|
382
|
+
self.stdout.write(f" Pending Tasks: {total_pending}")
|
383
|
+
self.stdout.write(f" Failed Tasks: {total_failed}")
|
384
|
+
|
385
|
+
self.stdout.write(f"\n🌐 Dashboard URL: http://localhost:8000/cfg/admin/django_cfg_tasks/admin/dashboard/")
|
386
|
+
|
387
|
+
# Показать ключи если запрошено
|
388
|
+
if options['show_keys']:
|
389
|
+
self.show_redis_keys(simulator)
|
390
|
+
|
391
|
+
except Exception as e:
|
392
|
+
raise CommandError(f"Simulation failed: {e}")
|
393
|
+
|
394
|
+
def show_summary(self, simulator: TaskSimulator):
|
395
|
+
"""Показать сводку текущих данных."""
|
396
|
+
self.stdout.write("📊 Current Redis Data Summary")
|
397
|
+
self.stdout.write("=" * 40)
|
398
|
+
|
399
|
+
summary = simulator.get_redis_summary()
|
400
|
+
|
401
|
+
self.stdout.write(f"Total Redis keys: {summary['total_keys']}")
|
402
|
+
self.stdout.write(f"Active workers: {summary['workers']}")
|
403
|
+
|
404
|
+
if summary['queues']:
|
405
|
+
self.stdout.write("\nQueues:")
|
406
|
+
for queue_name, counts in summary['queues'].items():
|
407
|
+
self.stdout.write(f" {queue_name}: {counts['pending']} pending, {counts['failed']} failed")
|
408
|
+
else:
|
409
|
+
self.stdout.write("\nNo active queues found")
|
410
|
+
|
411
|
+
if summary['statistics']:
|
412
|
+
stats = summary['statistics']
|
413
|
+
self.stdout.write(f"\nStatistics:")
|
414
|
+
self.stdout.write(f" Total processed: {stats.get('total_processed', 'N/A')}")
|
415
|
+
self.stdout.write(f" Total failed: {stats.get('total_failed', 'N/A')}")
|
416
|
+
self.stdout.write(f" Avg processing time: {stats.get('processing_time_avg', 'N/A')}s")
|
417
|
+
|
418
|
+
def show_redis_keys(self, simulator: TaskSimulator):
|
419
|
+
"""Показать все Redis ключи."""
|
420
|
+
self.stdout.write("\n🔍 Redis Keys:")
|
421
|
+
keys = simulator.redis_client.keys("dramatiq:*")
|
422
|
+
|
423
|
+
if not keys:
|
424
|
+
self.stdout.write(" No Dramatiq keys found")
|
425
|
+
return
|
426
|
+
|
427
|
+
for key in sorted(keys):
|
428
|
+
key_type = simulator.redis_client.type(key)
|
429
|
+
if key_type == 'list':
|
430
|
+
length = simulator.redis_client.llen(key)
|
431
|
+
self.stdout.write(f" {key} (list): {length} items")
|
432
|
+
elif key_type == 'string':
|
433
|
+
ttl = simulator.redis_client.ttl(key)
|
434
|
+
ttl_str = f"TTL {ttl}s" if ttl > 0 else "no TTL"
|
435
|
+
self.stdout.write(f" {key} (string): {ttl_str}")
|
436
|
+
else:
|
437
|
+
self.stdout.write(f" {key} ({key_type})")
|
@@ -0,0 +1,226 @@
|
|
1
|
+
"""
|
2
|
+
Django management command for clearing task queues.
|
3
|
+
|
4
|
+
This command provides utilities for clearing failed tasks,
|
5
|
+
specific queues, or all tasks from the Dramatiq system.
|
6
|
+
"""
|
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
|
+
from django_cfg.modules.django_logging import get_logger
|
13
|
+
|
14
|
+
logger = get_logger('task_clear')
|
15
|
+
|
16
|
+
|
17
|
+
class Command(BaseCommand):
|
18
|
+
"""
|
19
|
+
Clear tasks from Dramatiq queues.
|
20
|
+
|
21
|
+
Provides options to:
|
22
|
+
- Clear all tasks from specific queues
|
23
|
+
- Clear only failed tasks
|
24
|
+
- Clear all tasks from all queues
|
25
|
+
"""
|
26
|
+
|
27
|
+
# Web execution metadata
|
28
|
+
web_executable = False
|
29
|
+
requires_input = True
|
30
|
+
is_destructive = True
|
31
|
+
|
32
|
+
help = "Clear tasks from Dramatiq queues"
|
33
|
+
|
34
|
+
def add_arguments(self, parser):
|
35
|
+
"""Add command line arguments."""
|
36
|
+
parser.add_argument(
|
37
|
+
"--queue",
|
38
|
+
type=str,
|
39
|
+
help="Specific queue to clear (default: all queues)",
|
40
|
+
)
|
41
|
+
parser.add_argument(
|
42
|
+
"--failed-only",
|
43
|
+
action="store_true",
|
44
|
+
help="Clear only failed tasks",
|
45
|
+
)
|
46
|
+
parser.add_argument(
|
47
|
+
"--confirm",
|
48
|
+
action="store_true",
|
49
|
+
help="Skip confirmation prompt",
|
50
|
+
)
|
51
|
+
parser.add_argument(
|
52
|
+
"--dry-run",
|
53
|
+
action="store_true",
|
54
|
+
help="Show what would be cleared without actually clearing",
|
55
|
+
)
|
56
|
+
|
57
|
+
def handle(self, *args, **options):
|
58
|
+
"""Handle the command execution."""
|
59
|
+
logger.info("Starting task_clear command")
|
60
|
+
|
61
|
+
try:
|
62
|
+
# Import here to avoid issues if dramatiq is not installed
|
63
|
+
from django_cfg.modules.django_tasks import get_task_service
|
64
|
+
|
65
|
+
# Get task service
|
66
|
+
task_service = get_task_service()
|
67
|
+
|
68
|
+
# Check if task system is enabled
|
69
|
+
if not task_service.is_enabled():
|
70
|
+
raise CommandError(
|
71
|
+
"Task system is not enabled. "
|
72
|
+
"Please configure 'tasks' in your Django-CFG configuration."
|
73
|
+
)
|
74
|
+
|
75
|
+
# Get task manager
|
76
|
+
manager = task_service.manager
|
77
|
+
if not manager:
|
78
|
+
raise CommandError("Task manager not available")
|
79
|
+
|
80
|
+
# Get configuration
|
81
|
+
config = task_service.config
|
82
|
+
if not config:
|
83
|
+
raise CommandError("Task configuration not available")
|
84
|
+
|
85
|
+
# Determine queues to clear
|
86
|
+
if options.get("queue"):
|
87
|
+
queues_to_clear = [options["queue"]]
|
88
|
+
# Validate queue exists
|
89
|
+
if options["queue"] not in config.get_effective_queues():
|
90
|
+
self.stdout.write(
|
91
|
+
self.style.WARNING(
|
92
|
+
f"Queue '{options['queue']}' not in configured queues: "
|
93
|
+
f"{', '.join(config.get_effective_queues())}"
|
94
|
+
)
|
95
|
+
)
|
96
|
+
else:
|
97
|
+
queues_to_clear = config.get_effective_queues()
|
98
|
+
|
99
|
+
# Show what will be cleared
|
100
|
+
self._show_clear_plan(queues_to_clear, options)
|
101
|
+
|
102
|
+
if options["dry_run"]:
|
103
|
+
self.stdout.write(
|
104
|
+
self.style.SUCCESS("Dry run completed - no tasks were cleared")
|
105
|
+
)
|
106
|
+
return
|
107
|
+
|
108
|
+
# Confirm action
|
109
|
+
if not options["confirm"]:
|
110
|
+
if not self._confirm_clear(queues_to_clear, options):
|
111
|
+
self.stdout.write("Operation cancelled")
|
112
|
+
return
|
113
|
+
|
114
|
+
# Perform clearing
|
115
|
+
self._clear_queues(manager, queues_to_clear, options)
|
116
|
+
|
117
|
+
except ImportError:
|
118
|
+
raise CommandError(
|
119
|
+
"Dramatiq dependencies not installed. "
|
120
|
+
"Install with: pip install django-cfg[tasks]"
|
121
|
+
)
|
122
|
+
except Exception as e:
|
123
|
+
logger.exception("Failed to clear tasks")
|
124
|
+
raise CommandError(f"Failed to clear tasks: {e}")
|
125
|
+
|
126
|
+
def _show_clear_plan(self, queues: List[str], options):
|
127
|
+
"""Show what will be cleared."""
|
128
|
+
self.stdout.write(
|
129
|
+
self.style.SUCCESS("=== Clear Plan ===")
|
130
|
+
)
|
131
|
+
|
132
|
+
if options.get("failed_only"):
|
133
|
+
self.stdout.write("Action: Clear FAILED tasks only")
|
134
|
+
else:
|
135
|
+
self.stdout.write("Action: Clear ALL tasks")
|
136
|
+
|
137
|
+
self.stdout.write(f"Queues: {', '.join(queues)}")
|
138
|
+
|
139
|
+
# Get current queue statistics
|
140
|
+
try:
|
141
|
+
from django_cfg.modules.django_tasks import get_task_service
|
142
|
+
task_service = get_task_service()
|
143
|
+
manager = task_service.manager
|
144
|
+
|
145
|
+
if manager:
|
146
|
+
queue_stats = manager.get_queue_stats()
|
147
|
+
|
148
|
+
self.stdout.write("\nCurrent Queue Statistics:")
|
149
|
+
for stat in queue_stats:
|
150
|
+
if stat["name"] in queues:
|
151
|
+
name = stat["name"]
|
152
|
+
pending = stat.get("pending", 0)
|
153
|
+
failed = stat.get("failed", 0)
|
154
|
+
|
155
|
+
if options.get("failed_only"):
|
156
|
+
self.stdout.write(f" {name}: {failed} failed tasks")
|
157
|
+
else:
|
158
|
+
total = pending + stat.get("running", 0) + failed
|
159
|
+
self.stdout.write(f" {name}: {total} total tasks")
|
160
|
+
except Exception as e:
|
161
|
+
self.stdout.write(f"Could not get queue statistics: {e}")
|
162
|
+
|
163
|
+
self.stdout.write()
|
164
|
+
|
165
|
+
def _confirm_clear(self, queues: List[str], options) -> bool:
|
166
|
+
"""Confirm the clear operation with user."""
|
167
|
+
if options.get("failed_only"):
|
168
|
+
action = "clear FAILED tasks"
|
169
|
+
else:
|
170
|
+
action = "clear ALL tasks"
|
171
|
+
|
172
|
+
queue_list = ", ".join(queues)
|
173
|
+
|
174
|
+
self.stdout.write(
|
175
|
+
self.style.WARNING(
|
176
|
+
f"This will {action} from queues: {queue_list}"
|
177
|
+
)
|
178
|
+
)
|
179
|
+
|
180
|
+
response = input("Are you sure? [y/N]: ").lower().strip()
|
181
|
+
return response in ["y", "yes"]
|
182
|
+
|
183
|
+
def _clear_queues(self, manager, queues: List[str], options):
|
184
|
+
"""Clear the specified queues."""
|
185
|
+
cleared_count = 0
|
186
|
+
|
187
|
+
self.stdout.write(
|
188
|
+
self.style.SUCCESS("Starting queue clearing...")
|
189
|
+
)
|
190
|
+
|
191
|
+
for queue_name in queues:
|
192
|
+
try:
|
193
|
+
self.stdout.write(f"Clearing queue: {queue_name}")
|
194
|
+
|
195
|
+
if options.get("failed_only"):
|
196
|
+
# Clear only failed tasks
|
197
|
+
# TODO: Implement failed task clearing
|
198
|
+
# This would require specific Dramatiq broker methods
|
199
|
+
count = 0 # Placeholder
|
200
|
+
self.stdout.write(f" Cleared {count} failed tasks")
|
201
|
+
else:
|
202
|
+
# Clear all tasks
|
203
|
+
success = manager.clear_queue(queue_name)
|
204
|
+
if success:
|
205
|
+
# TODO: Get actual count of cleared tasks
|
206
|
+
count = 0 # Placeholder
|
207
|
+
self.stdout.write(f" Cleared {count} tasks")
|
208
|
+
cleared_count += count
|
209
|
+
else:
|
210
|
+
self.stdout.write(
|
211
|
+
self.style.ERROR(f" Failed to clear queue: {queue_name}")
|
212
|
+
)
|
213
|
+
|
214
|
+
except Exception as e:
|
215
|
+
self.stdout.write(
|
216
|
+
self.style.ERROR(f" Error clearing queue {queue_name}: {e}")
|
217
|
+
)
|
218
|
+
|
219
|
+
if cleared_count > 0:
|
220
|
+
self.stdout.write(
|
221
|
+
self.style.SUCCESS(f"Successfully cleared {cleared_count} tasks")
|
222
|
+
)
|
223
|
+
else:
|
224
|
+
self.stdout.write(
|
225
|
+
self.style.WARNING("No tasks were cleared")
|
226
|
+
)
|