django-cfg 1.3.9__py3-none-any.whl → 1.3.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/__init__.py +1 -1
- django_cfg/apps/payments/admin/networks_admin.py +12 -1
- django_cfg/apps/payments/admin/payments_admin.py +13 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +62 -14
- django_cfg/apps/payments/admin_interface/templates/payments/components/payment_card.html +121 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/payment_qr_code.html +95 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/progress_bar.html +37 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_stats.html +60 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_badge.html +41 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_overview.html +83 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_detail.html +363 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +33 -3
- django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +96 -45
- django_cfg/apps/payments/admin_interface/views/forms.py +5 -1
- django_cfg/apps/payments/config/__init__.py +14 -15
- django_cfg/apps/payments/config/django_cfg_integration.py +59 -1
- django_cfg/apps/payments/config/helpers.py +8 -13
- django_cfg/apps/payments/migrations/0001_initial.py +33 -46
- django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py +46 -0
- django_cfg/apps/payments/migrations/0003_universalpayment_status_changed_at.py +25 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +142 -25
- django_cfg/apps/payments/models/payments.py +94 -0
- django_cfg/apps/payments/services/core/base.py +4 -4
- django_cfg/apps/payments/services/core/payment_service.py +265 -38
- django_cfg/apps/payments/services/providers/base.py +209 -3
- django_cfg/apps/payments/services/providers/models/__init__.py +2 -0
- django_cfg/apps/payments/services/providers/models/base.py +25 -2
- django_cfg/apps/payments/services/providers/nowpayments/models.py +2 -2
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +57 -9
- django_cfg/apps/payments/services/providers/registry.py +5 -5
- django_cfg/apps/payments/services/types/requests.py +19 -7
- django_cfg/apps/payments/signals/payment_signals.py +31 -2
- django_cfg/apps/payments/static/payments/js/api-client.js +6 -1
- django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
- django_cfg/apps/payments/static/payments/js/payment-form.js +35 -26
- django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
- django_cfg/apps/payments/urls.py +3 -2
- django_cfg/apps/payments/views/api/currencies.py +3 -0
- django_cfg/apps/payments/views/serializers/currencies.py +18 -5
- django_cfg/apps/tasks/admin/tasks_admin.py +2 -2
- django_cfg/apps/tasks/static/tasks/css/dashboard.css +68 -217
- django_cfg/apps/tasks/static/tasks/js/api.js +40 -84
- django_cfg/apps/tasks/static/tasks/js/components/DataManager.js +24 -0
- django_cfg/apps/tasks/static/tasks/js/components/TabManager.js +85 -0
- django_cfg/apps/tasks/static/tasks/js/components/TaskRenderer.js +216 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/main.mjs +245 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/overview.mjs +123 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/queues.mjs +120 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/tasks.mjs +350 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/workers.mjs +169 -0
- django_cfg/apps/tasks/tasks/__init__.py +10 -0
- django_cfg/apps/tasks/tasks/demo_tasks.py +133 -0
- django_cfg/apps/tasks/templates/tasks/components/management_actions.html +42 -45
- django_cfg/apps/tasks/templates/tasks/components/{status_cards.html → overview_content.html} +30 -11
- django_cfg/apps/tasks/templates/tasks/components/queues_content.html +19 -0
- django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +16 -10
- django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +51 -0
- django_cfg/apps/tasks/templates/tasks/components/workers_content.html +30 -0
- django_cfg/apps/tasks/templates/tasks/layout/base.html +117 -0
- django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +82 -0
- django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +40 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +37 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +41 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +50 -0
- django_cfg/apps/tasks/urls.py +2 -2
- django_cfg/apps/tasks/urls_admin.py +2 -2
- django_cfg/apps/tasks/utils/__init__.py +1 -0
- django_cfg/apps/tasks/utils/simulator.py +356 -0
- django_cfg/apps/tasks/views/__init__.py +16 -0
- django_cfg/apps/tasks/views/api.py +569 -0
- django_cfg/apps/tasks/views/dashboard.py +58 -0
- django_cfg/core/integration/__init__.py +21 -0
- django_cfg/management/commands/rundramatiq_simulator.py +430 -0
- django_cfg/models/constance.py +0 -11
- django_cfg/models/payments.py +137 -3
- django_cfg/modules/django_tasks.py +54 -21
- django_cfg/registry/core.py +4 -9
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/METADATA +2 -2
- {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/RECORD +84 -152
- django_cfg/apps/payments/config/constance/__init__.py +0 -22
- django_cfg/apps/payments/config/constance/config_service.py +0 -123
- django_cfg/apps/payments/config/constance/fields.py +0 -69
- django_cfg/apps/payments/config/constance/settings.py +0 -160
- django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +0 -26
- django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +0 -28
- django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +0 -30
- django_cfg/apps/tasks/static/tasks/js/dashboard.js +0 -614
- django_cfg/apps/tasks/static/tasks/js/modals.js +0 -452
- django_cfg/apps/tasks/static/tasks/js/notifications.js +0 -144
- django_cfg/apps/tasks/static/tasks/js/task-monitor.js +0 -454
- django_cfg/apps/tasks/static/tasks/js/theme.js +0 -77
- django_cfg/apps/tasks/templates/tasks/base.html +0 -96
- django_cfg/apps/tasks/templates/tasks/components/info_cards.html +0 -85
- django_cfg/apps/tasks/templates/tasks/components/overview_tab.html +0 -22
- django_cfg/apps/tasks/templates/tasks/components/queues_tab.html +0 -19
- django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -103
- django_cfg/apps/tasks/templates/tasks/components/tasks_tab.html +0 -32
- django_cfg/apps/tasks/templates/tasks/components/workers_tab.html +0 -29
- django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -29
- django_cfg/apps/tasks/views.py +0 -461
- django_cfg/management/commands/app_agent_diagnose.py +0 -470
- django_cfg/management/commands/app_agent_generate.py +0 -342
- django_cfg/management/commands/app_agent_info.py +0 -308
- django_cfg/management/commands/auto_generate.py +0 -486
- django_cfg/modules/django_app_agent/__init__.py +0 -87
- django_cfg/modules/django_app_agent/agents/__init__.py +0 -40
- django_cfg/modules/django_app_agent/agents/base/__init__.py +0 -24
- django_cfg/modules/django_app_agent/agents/base/agent.py +0 -354
- django_cfg/modules/django_app_agent/agents/base/context.py +0 -236
- django_cfg/modules/django_app_agent/agents/base/executor.py +0 -430
- django_cfg/modules/django_app_agent/agents/generation/__init__.py +0 -12
- django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +0 -15
- django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +0 -147
- django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +0 -99
- django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +0 -32
- django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +0 -290
- django_cfg/modules/django_app_agent/agents/interfaces.py +0 -376
- django_cfg/modules/django_app_agent/core/__init__.py +0 -33
- django_cfg/modules/django_app_agent/core/config.py +0 -300
- django_cfg/modules/django_app_agent/core/exceptions.py +0 -359
- django_cfg/modules/django_app_agent/models/__init__.py +0 -71
- django_cfg/modules/django_app_agent/models/base.py +0 -283
- django_cfg/modules/django_app_agent/models/context.py +0 -496
- django_cfg/modules/django_app_agent/models/enums.py +0 -481
- django_cfg/modules/django_app_agent/models/requests.py +0 -500
- django_cfg/modules/django_app_agent/models/responses.py +0 -585
- django_cfg/modules/django_app_agent/pytest.ini +0 -6
- django_cfg/modules/django_app_agent/services/__init__.py +0 -42
- django_cfg/modules/django_app_agent/services/app_generator/__init__.py +0 -30
- django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +0 -133
- django_cfg/modules/django_app_agent/services/app_generator/context.py +0 -40
- django_cfg/modules/django_app_agent/services/app_generator/main.py +0 -202
- django_cfg/modules/django_app_agent/services/app_generator/structure.py +0 -316
- django_cfg/modules/django_app_agent/services/app_generator/validation.py +0 -125
- django_cfg/modules/django_app_agent/services/base.py +0 -437
- django_cfg/modules/django_app_agent/services/context_builder/__init__.py +0 -34
- django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +0 -141
- django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +0 -276
- django_cfg/modules/django_app_agent/services/context_builder/main.py +0 -272
- django_cfg/modules/django_app_agent/services/context_builder/models.py +0 -40
- django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +0 -85
- django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +0 -31
- django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +0 -311
- django_cfg/modules/django_app_agent/services/project_scanner/main.py +0 -221
- django_cfg/modules/django_app_agent/services/project_scanner/models.py +0 -59
- django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +0 -94
- django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +0 -28
- django_cfg/modules/django_app_agent/services/questioning_service/main.py +0 -273
- django_cfg/modules/django_app_agent/services/questioning_service/models.py +0 -111
- django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +0 -251
- django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +0 -347
- django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +0 -356
- django_cfg/modules/django_app_agent/services/report_service.py +0 -332
- django_cfg/modules/django_app_agent/services/template_manager/__init__.py +0 -18
- django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +0 -236
- django_cfg/modules/django_app_agent/services/template_manager/main.py +0 -159
- django_cfg/modules/django_app_agent/services/template_manager/models.py +0 -36
- django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +0 -100
- django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +0 -105
- django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +0 -31
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +0 -44
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +0 -81
- django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +0 -107
- django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +0 -139
- django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +0 -91
- django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +0 -195
- django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +0 -35
- django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +0 -211
- django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +0 -200
- django_cfg/modules/django_app_agent/services/validation_service/__init__.py +0 -25
- django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +0 -333
- django_cfg/modules/django_app_agent/services/validation_service/main.py +0 -242
- django_cfg/modules/django_app_agent/services/validation_service/models.py +0 -66
- django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +0 -352
- django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +0 -272
- django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +0 -203
- django_cfg/modules/django_app_agent/ui/__init__.py +0 -25
- django_cfg/modules/django_app_agent/ui/cli.py +0 -419
- django_cfg/modules/django_app_agent/ui/rich_components.py +0 -622
- django_cfg/modules/django_app_agent/utils/__init__.py +0 -38
- django_cfg/modules/django_app_agent/utils/logging.py +0 -360
- django_cfg/modules/django_app_agent/utils/validation.py +0 -417
- {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,430 @@
|
|
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']
|
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
|
+
}
|
93
|
+
|
94
|
+
if failed_tasks_per_queue is None:
|
95
|
+
failed_tasks_per_queue = {
|
96
|
+
'critical': 0,
|
97
|
+
'high': 1,
|
98
|
+
'default': 3,
|
99
|
+
'low': 2,
|
100
|
+
'background': 1,
|
101
|
+
'payments': 0,
|
102
|
+
'agents': 1
|
103
|
+
}
|
104
|
+
|
105
|
+
results = {}
|
106
|
+
|
107
|
+
for queue_name in self.queues:
|
108
|
+
queue_results = {'pending': 0, 'failed': 0}
|
109
|
+
|
110
|
+
# Pending tasks
|
111
|
+
pending_count = pending_tasks_per_queue.get(queue_name, 0)
|
112
|
+
if pending_count > 0:
|
113
|
+
queue_key = f"dramatiq:default.DQ.{queue_name}"
|
114
|
+
|
115
|
+
# Add fake tasks to queue
|
116
|
+
for i in range(pending_count):
|
117
|
+
task_data = {
|
118
|
+
"queue_name": queue_name,
|
119
|
+
"actor_name": f"process_{queue_name}_task",
|
120
|
+
"args": [f"task_{i}"],
|
121
|
+
"kwargs": {},
|
122
|
+
"options": {},
|
123
|
+
"message_id": f"msg_{queue_name}_{i}_{int(time.time())}",
|
124
|
+
"message_timestamp": int(time.time() * 1000)
|
125
|
+
}
|
126
|
+
self.redis_client.lpush(queue_key, json.dumps(task_data))
|
127
|
+
|
128
|
+
queue_results['pending'] = pending_count
|
129
|
+
|
130
|
+
# Failed tasks
|
131
|
+
failed_count = failed_tasks_per_queue.get(queue_name, 0)
|
132
|
+
if failed_count > 0:
|
133
|
+
failed_key = f"dramatiq:default.DQ.{queue_name}.failed"
|
134
|
+
|
135
|
+
# Add fake failed tasks
|
136
|
+
for i in range(failed_count):
|
137
|
+
failed_task_data = {
|
138
|
+
"queue_name": queue_name,
|
139
|
+
"actor_name": f"failed_{queue_name}_task",
|
140
|
+
"args": [f"failed_task_{i}"],
|
141
|
+
"kwargs": {},
|
142
|
+
"options": {},
|
143
|
+
"message_id": f"failed_msg_{queue_name}_{i}_{int(time.time())}",
|
144
|
+
"message_timestamp": int(time.time() * 1000),
|
145
|
+
"error": f"Simulated error for {queue_name} task {i}"
|
146
|
+
}
|
147
|
+
self.redis_client.lpush(failed_key, json.dumps(failed_task_data))
|
148
|
+
|
149
|
+
queue_results['failed'] = failed_count
|
150
|
+
|
151
|
+
if queue_results['pending'] > 0 or queue_results['failed'] > 0:
|
152
|
+
results[queue_name] = queue_results
|
153
|
+
|
154
|
+
return results
|
155
|
+
|
156
|
+
def simulate_workers(self, worker_count=3) -> list:
|
157
|
+
"""
|
158
|
+
Симулировать активных воркеров.
|
159
|
+
|
160
|
+
Args:
|
161
|
+
worker_count: Количество воркеров для симуляции
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
Список ID созданных воркеров
|
165
|
+
"""
|
166
|
+
worker_ids = []
|
167
|
+
|
168
|
+
for i in range(worker_count):
|
169
|
+
worker_id = f"worker_{i}_{int(time.time())}"
|
170
|
+
worker_key = f"dramatiq:worker:{worker_id}"
|
171
|
+
|
172
|
+
worker_data = {
|
173
|
+
"worker_id": worker_id,
|
174
|
+
"hostname": f"localhost",
|
175
|
+
"pid": 1000 + i,
|
176
|
+
"queues": self.queues,
|
177
|
+
"started_at": datetime.now(timezone.utc).isoformat(),
|
178
|
+
"last_heartbeat": datetime.now(timezone.utc).isoformat(),
|
179
|
+
"status": "active"
|
180
|
+
}
|
181
|
+
|
182
|
+
# Устанавливаем данные воркера с TTL
|
183
|
+
self.redis_client.setex(
|
184
|
+
worker_key,
|
185
|
+
300, # 5 минут TTL
|
186
|
+
json.dumps(worker_data)
|
187
|
+
)
|
188
|
+
|
189
|
+
worker_ids.append(worker_id)
|
190
|
+
|
191
|
+
return worker_ids
|
192
|
+
|
193
|
+
def simulate_task_statistics(self) -> Dict[str, Any]:
|
194
|
+
"""
|
195
|
+
Симулировать статистику задач.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
Созданная статистика
|
199
|
+
"""
|
200
|
+
stats_data = {
|
201
|
+
"total_processed": random.randint(1000, 2000),
|
202
|
+
"total_failed": random.randint(30, 80),
|
203
|
+
"total_retried": random.randint(15, 40),
|
204
|
+
"processing_time_avg": round(random.uniform(1.5, 4.0), 2),
|
205
|
+
"last_updated": datetime.now(timezone.utc).isoformat()
|
206
|
+
}
|
207
|
+
|
208
|
+
stats_key = "dramatiq:stats"
|
209
|
+
self.redis_client.setex(stats_key, 3600, json.dumps(stats_data))
|
210
|
+
|
211
|
+
return stats_data
|
212
|
+
|
213
|
+
def run_simulation(self, workers=3, clear_first=True) -> Dict[str, Any]:
|
214
|
+
"""
|
215
|
+
Запустить полную симуляцию.
|
216
|
+
|
217
|
+
Args:
|
218
|
+
workers: Количество воркеров
|
219
|
+
clear_first: Очистить данные перед симуляцией
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
Результаты симуляции
|
223
|
+
"""
|
224
|
+
results = {
|
225
|
+
'cleared_keys': 0,
|
226
|
+
'queues': {},
|
227
|
+
'workers': [],
|
228
|
+
'statistics': {}
|
229
|
+
}
|
230
|
+
|
231
|
+
if clear_first:
|
232
|
+
results['cleared_keys'] = self.clear_all_data()
|
233
|
+
|
234
|
+
results['queues'] = self.simulate_queues()
|
235
|
+
results['workers'] = self.simulate_workers(workers)
|
236
|
+
results['statistics'] = self.simulate_task_statistics()
|
237
|
+
|
238
|
+
return results
|
239
|
+
|
240
|
+
def get_redis_summary(self) -> Dict[str, Any]:
|
241
|
+
"""Получить сводку по данным в Redis."""
|
242
|
+
summary = {
|
243
|
+
'total_keys': 0,
|
244
|
+
'queues': {},
|
245
|
+
'workers': 0,
|
246
|
+
'statistics': None
|
247
|
+
}
|
248
|
+
|
249
|
+
# Подсчитываем все ключи
|
250
|
+
all_keys = self.redis_client.keys("dramatiq:*")
|
251
|
+
summary['total_keys'] = len(all_keys)
|
252
|
+
|
253
|
+
# Анализируем очереди
|
254
|
+
for queue_name in self.queues:
|
255
|
+
pending_key = f"dramatiq:default.DQ.{queue_name}"
|
256
|
+
failed_key = f"dramatiq:default.DQ.{queue_name}.failed"
|
257
|
+
|
258
|
+
pending = self.redis_client.llen(pending_key)
|
259
|
+
failed = self.redis_client.llen(failed_key)
|
260
|
+
|
261
|
+
if pending > 0 or failed > 0:
|
262
|
+
summary['queues'][queue_name] = {
|
263
|
+
'pending': pending,
|
264
|
+
'failed': failed
|
265
|
+
}
|
266
|
+
|
267
|
+
# Подсчитываем воркеров
|
268
|
+
worker_keys = self.redis_client.keys("dramatiq:worker:*")
|
269
|
+
summary['workers'] = len(worker_keys)
|
270
|
+
|
271
|
+
# Получаем статистику
|
272
|
+
stats_key = "dramatiq:stats"
|
273
|
+
if self.redis_client.exists(stats_key):
|
274
|
+
try:
|
275
|
+
stats_data = self.redis_client.get(stats_key)
|
276
|
+
summary['statistics'] = json.loads(stats_data)
|
277
|
+
except:
|
278
|
+
pass
|
279
|
+
|
280
|
+
return summary
|
281
|
+
|
282
|
+
|
283
|
+
class Command(BaseCommand):
|
284
|
+
"""Django management command для симуляции Dramatiq данных."""
|
285
|
+
|
286
|
+
help = 'Simulate Dramatiq tasks and workers for dashboard testing'
|
287
|
+
|
288
|
+
def add_arguments(self, parser):
|
289
|
+
"""Добавить аргументы команды."""
|
290
|
+
parser.add_argument(
|
291
|
+
'--workers',
|
292
|
+
type=int,
|
293
|
+
default=3,
|
294
|
+
help='Number of workers to simulate (default: 3)'
|
295
|
+
)
|
296
|
+
|
297
|
+
parser.add_argument(
|
298
|
+
'--no-clear',
|
299
|
+
action='store_true',
|
300
|
+
help='Do not clear existing data before simulation'
|
301
|
+
)
|
302
|
+
|
303
|
+
parser.add_argument(
|
304
|
+
'--clear-only',
|
305
|
+
action='store_true',
|
306
|
+
help='Only clear data, do not simulate'
|
307
|
+
)
|
308
|
+
|
309
|
+
parser.add_argument(
|
310
|
+
'--show-keys',
|
311
|
+
action='store_true',
|
312
|
+
help='Show Redis keys after operation'
|
313
|
+
)
|
314
|
+
|
315
|
+
parser.add_argument(
|
316
|
+
'--summary',
|
317
|
+
action='store_true',
|
318
|
+
help='Show summary of current Redis data'
|
319
|
+
)
|
320
|
+
|
321
|
+
def handle(self, *args, **options):
|
322
|
+
"""Выполнить команду."""
|
323
|
+
try:
|
324
|
+
simulator = TaskSimulator()
|
325
|
+
|
326
|
+
# Показать только сводку
|
327
|
+
if options['summary']:
|
328
|
+
self.show_summary(simulator)
|
329
|
+
return
|
330
|
+
|
331
|
+
# Только очистка
|
332
|
+
if options['clear_only']:
|
333
|
+
self.stdout.write("🧹 Clearing all test data...")
|
334
|
+
cleared = simulator.clear_all_data()
|
335
|
+
self.stdout.write(
|
336
|
+
self.style.SUCCESS(f"✅ Cleared {cleared} Redis keys")
|
337
|
+
)
|
338
|
+
return
|
339
|
+
|
340
|
+
# Полная симуляция
|
341
|
+
self.stdout.write("🎭 Starting Dramatiq Task Simulation")
|
342
|
+
self.stdout.write("=" * 50)
|
343
|
+
|
344
|
+
results = simulator.run_simulation(
|
345
|
+
workers=options['workers'],
|
346
|
+
clear_first=not options['no_clear']
|
347
|
+
)
|
348
|
+
|
349
|
+
# Показываем результаты
|
350
|
+
if results['cleared_keys'] > 0:
|
351
|
+
self.stdout.write(f"🧹 Cleared {results['cleared_keys']} existing keys")
|
352
|
+
|
353
|
+
self.stdout.write(f"📋 Created queues:")
|
354
|
+
total_pending = 0
|
355
|
+
total_failed = 0
|
356
|
+
|
357
|
+
for queue_name, counts in results['queues'].items():
|
358
|
+
pending = counts['pending']
|
359
|
+
failed = counts['failed']
|
360
|
+
total_pending += pending
|
361
|
+
total_failed += failed
|
362
|
+
|
363
|
+
self.stdout.write(f" {queue_name}: {pending} pending, {failed} failed")
|
364
|
+
|
365
|
+
self.stdout.write(f"👷 Created {len(results['workers'])} workers")
|
366
|
+
self.stdout.write(f"📊 Added task statistics")
|
367
|
+
|
368
|
+
self.stdout.write("=" * 50)
|
369
|
+
self.stdout.write(self.style.SUCCESS("✅ Simulation completed!"))
|
370
|
+
|
371
|
+
self.stdout.write(f"\n📊 Summary:")
|
372
|
+
active_queues = len(results['queues'])
|
373
|
+
self.stdout.write(f" Active Queues: {active_queues}")
|
374
|
+
self.stdout.write(f" Active Workers: {len(results['workers'])}")
|
375
|
+
self.stdout.write(f" Pending Tasks: {total_pending}")
|
376
|
+
self.stdout.write(f" Failed Tasks: {total_failed}")
|
377
|
+
|
378
|
+
self.stdout.write(f"\n🌐 Dashboard URL: http://localhost:8000/cfg/admin/django_cfg_tasks/admin/dashboard/")
|
379
|
+
|
380
|
+
# Показать ключи если запрошено
|
381
|
+
if options['show_keys']:
|
382
|
+
self.show_redis_keys(simulator)
|
383
|
+
|
384
|
+
except Exception as e:
|
385
|
+
raise CommandError(f"Simulation failed: {e}")
|
386
|
+
|
387
|
+
def show_summary(self, simulator: TaskSimulator):
|
388
|
+
"""Показать сводку текущих данных."""
|
389
|
+
self.stdout.write("📊 Current Redis Data Summary")
|
390
|
+
self.stdout.write("=" * 40)
|
391
|
+
|
392
|
+
summary = simulator.get_redis_summary()
|
393
|
+
|
394
|
+
self.stdout.write(f"Total Redis keys: {summary['total_keys']}")
|
395
|
+
self.stdout.write(f"Active workers: {summary['workers']}")
|
396
|
+
|
397
|
+
if summary['queues']:
|
398
|
+
self.stdout.write("\nQueues:")
|
399
|
+
for queue_name, counts in summary['queues'].items():
|
400
|
+
self.stdout.write(f" {queue_name}: {counts['pending']} pending, {counts['failed']} failed")
|
401
|
+
else:
|
402
|
+
self.stdout.write("\nNo active queues found")
|
403
|
+
|
404
|
+
if summary['statistics']:
|
405
|
+
stats = summary['statistics']
|
406
|
+
self.stdout.write(f"\nStatistics:")
|
407
|
+
self.stdout.write(f" Total processed: {stats.get('total_processed', 'N/A')}")
|
408
|
+
self.stdout.write(f" Total failed: {stats.get('total_failed', 'N/A')}")
|
409
|
+
self.stdout.write(f" Avg processing time: {stats.get('processing_time_avg', 'N/A')}s")
|
410
|
+
|
411
|
+
def show_redis_keys(self, simulator: TaskSimulator):
|
412
|
+
"""Показать все Redis ключи."""
|
413
|
+
self.stdout.write("\n🔍 Redis Keys:")
|
414
|
+
keys = simulator.redis_client.keys("dramatiq:*")
|
415
|
+
|
416
|
+
if not keys:
|
417
|
+
self.stdout.write(" No Dramatiq keys found")
|
418
|
+
return
|
419
|
+
|
420
|
+
for key in sorted(keys):
|
421
|
+
key_type = simulator.redis_client.type(key)
|
422
|
+
if key_type == 'list':
|
423
|
+
length = simulator.redis_client.llen(key)
|
424
|
+
self.stdout.write(f" {key} (list): {length} items")
|
425
|
+
elif key_type == 'string':
|
426
|
+
ttl = simulator.redis_client.ttl(key)
|
427
|
+
ttl_str = f"TTL {ttl}s" if ttl > 0 else "no TTL"
|
428
|
+
self.stdout.write(f" {key} (string): {ttl_str}")
|
429
|
+
else:
|
430
|
+
self.stdout.write(f" {key} ({key_type})")
|
django_cfg/models/constance.py
CHANGED
@@ -181,17 +181,6 @@ class ConstanceConfig(BaseModel, BaseCfgAutoModule):
|
|
181
181
|
except (ImportError, Exception):
|
182
182
|
pass
|
183
183
|
|
184
|
-
# Get fields from payments app (only if enabled)
|
185
|
-
if config and config.payments and config.payments.enabled:
|
186
|
-
try:
|
187
|
-
from django_cfg.apps.payments.config import get_django_cfg_payments_constance_fields
|
188
|
-
payments_fields = get_django_cfg_payments_constance_fields()
|
189
|
-
app_fields.extend(payments_fields)
|
190
|
-
print(f"✅ Added {len(payments_fields)} payments fields to Constance")
|
191
|
-
except (ImportError, Exception) as e:
|
192
|
-
print(f"❌ Failed to load payments constance fields: {e}")
|
193
|
-
traceback.print_exc()
|
194
|
-
|
195
184
|
# Cache the result
|
196
185
|
self._app_fields_cache = app_fields
|
197
186
|
return app_fields
|
django_cfg/models/payments.py
CHANGED
@@ -6,14 +6,110 @@ Simple, clean configuration following django-cfg philosophy.
|
|
6
6
|
|
7
7
|
from pydantic import BaseModel, Field
|
8
8
|
from typing import List, Dict, Optional
|
9
|
+
from django_cfg.models.cfg import BaseCfgAutoModule
|
9
10
|
|
10
11
|
|
11
|
-
class
|
12
|
+
class BaseProviderConfig(BaseModel):
|
13
|
+
"""Base configuration for payment providers."""
|
14
|
+
|
15
|
+
provider_name: str = Field(..., description="Provider name")
|
16
|
+
enabled: bool = Field(default=True, description="Whether provider is enabled")
|
17
|
+
|
18
|
+
def get_provider_config(self) -> Dict[str, any]:
|
19
|
+
"""Get provider-specific configuration."""
|
20
|
+
return {
|
21
|
+
'provider_name': self.provider_name,
|
22
|
+
'enabled': self.enabled,
|
23
|
+
}
|
24
|
+
|
25
|
+
|
26
|
+
class NowPaymentsProviderConfig(BaseProviderConfig):
|
27
|
+
"""NowPayments provider configuration."""
|
28
|
+
|
29
|
+
provider_name: str = Field(default="nowpayments", description="Provider name")
|
30
|
+
api_key: str = Field(default="", description="NowPayments API key")
|
31
|
+
ipn_secret: str = Field(default="", description="NowPayments IPN secret for webhook validation")
|
32
|
+
sandbox_mode: bool = Field(default=True, description="NowPayments sandbox mode")
|
33
|
+
|
34
|
+
def get_provider_config(self) -> Dict[str, any]:
|
35
|
+
"""Get NowPayments-specific configuration."""
|
36
|
+
return {
|
37
|
+
'provider_name': self.provider_name,
|
38
|
+
'enabled': self.enabled and bool(self.api_key.strip()),
|
39
|
+
'api_key': self.api_key,
|
40
|
+
'ipn_secret': self.ipn_secret,
|
41
|
+
'sandbox_mode': self.sandbox_mode,
|
42
|
+
}
|
43
|
+
|
44
|
+
|
45
|
+
# Future provider configs (commented out for now)
|
46
|
+
# class StripeProviderConfig(BaseProviderConfig):
|
47
|
+
# """Stripe provider configuration."""
|
48
|
+
#
|
49
|
+
# provider_name: str = Field(default="stripe", description="Provider name")
|
50
|
+
# api_key: str = Field(default="", description="Stripe API key")
|
51
|
+
# webhook_secret: str = Field(default="", description="Stripe webhook secret")
|
52
|
+
#
|
53
|
+
# def get_provider_config(self) -> Dict[str, any]:
|
54
|
+
# return {
|
55
|
+
# 'provider_name': self.provider_name,
|
56
|
+
# 'enabled': self.enabled and bool(self.api_key.strip()),
|
57
|
+
# 'api_key': self.api_key,
|
58
|
+
# 'webhook_secret': self.webhook_secret,
|
59
|
+
# }
|
60
|
+
|
61
|
+
|
62
|
+
class ProviderAPIKeysConfig(BaseModel):
|
63
|
+
"""
|
64
|
+
API keys configuration for payment providers.
|
65
|
+
|
66
|
+
Stores list of provider configurations.
|
67
|
+
"""
|
68
|
+
|
69
|
+
providers: List[BaseProviderConfig] = Field(
|
70
|
+
default_factory=list,
|
71
|
+
description="List of provider configurations"
|
72
|
+
)
|
73
|
+
|
74
|
+
def add_provider(self, provider_config: BaseProviderConfig):
|
75
|
+
"""Add a provider configuration."""
|
76
|
+
# Remove existing provider with same name
|
77
|
+
self.providers = [p for p in self.providers if p.provider_name != provider_config.provider_name]
|
78
|
+
self.providers.append(provider_config)
|
79
|
+
|
80
|
+
def get_provider_config(self, provider: str) -> Dict[str, any]:
|
81
|
+
"""Get provider-specific configuration."""
|
82
|
+
provider_lower = provider.lower()
|
83
|
+
|
84
|
+
for provider_config in self.providers:
|
85
|
+
if provider_config.provider_name.lower() == provider_lower:
|
86
|
+
return provider_config.get_provider_config()
|
87
|
+
|
88
|
+
return {'enabled': False, 'provider_name': provider}
|
89
|
+
|
90
|
+
def get_enabled_providers(self) -> List[str]:
|
91
|
+
"""Get list of enabled providers."""
|
92
|
+
enabled = []
|
93
|
+
for provider_config in self.providers:
|
94
|
+
config = provider_config.get_provider_config()
|
95
|
+
if config.get('enabled', False):
|
96
|
+
enabled.append(provider_config.provider_name)
|
97
|
+
return enabled
|
98
|
+
|
99
|
+
def get_provider_by_name(self, provider_name: str) -> Optional[BaseProviderConfig]:
|
100
|
+
"""Get provider configuration by name."""
|
101
|
+
for provider_config in self.providers:
|
102
|
+
if provider_config.provider_name.lower() == provider_name.lower():
|
103
|
+
return provider_config
|
104
|
+
return None
|
105
|
+
|
106
|
+
|
107
|
+
class PaymentsConfig(BaseModel, BaseCfgAutoModule):
|
12
108
|
"""
|
13
109
|
Payments app configuration for django-cfg.
|
14
110
|
|
15
|
-
|
16
|
-
|
111
|
+
Includes both static configuration and API keys.
|
112
|
+
API keys are now managed through BaseCfgAutoModule instead of Constance.
|
17
113
|
"""
|
18
114
|
|
19
115
|
# Core settings
|
@@ -22,6 +118,12 @@ class PaymentsConfig(BaseModel):
|
|
22
118
|
description="Enable payments app"
|
23
119
|
)
|
24
120
|
|
121
|
+
# API keys configuration
|
122
|
+
api_keys: ProviderAPIKeysConfig = Field(
|
123
|
+
default_factory=ProviderAPIKeysConfig,
|
124
|
+
description="API keys and secrets for payment providers"
|
125
|
+
)
|
126
|
+
|
25
127
|
# Middleware settings
|
26
128
|
middleware_enabled: bool = Field(
|
27
129
|
default=True,
|
@@ -207,6 +309,29 @@ class PaymentsConfig(BaseModel):
|
|
207
309
|
|
208
310
|
return items
|
209
311
|
|
312
|
+
# API Keys access methods
|
313
|
+
def get_provider_api_config(self, provider: str) -> Dict[str, any]:
|
314
|
+
"""Get provider-specific API configuration."""
|
315
|
+
return self.api_keys.get_provider_config(provider)
|
316
|
+
|
317
|
+
def get_enabled_providers(self) -> List[str]:
|
318
|
+
"""Get list of enabled providers (those with API keys configured)."""
|
319
|
+
return self.api_keys.get_enabled_providers()
|
320
|
+
|
321
|
+
def is_provider_enabled(self, provider: str) -> bool:
|
322
|
+
"""Check if a specific provider is enabled (has API keys configured)."""
|
323
|
+
config = self.api_keys.get_provider_config(provider)
|
324
|
+
return config.get('enabled', False)
|
325
|
+
|
326
|
+
# BaseCfgAutoModule implementation
|
327
|
+
def get_smart_defaults(self):
|
328
|
+
"""Get smart default configuration for this module."""
|
329
|
+
return PaymentsConfig()
|
330
|
+
|
331
|
+
def get_module_config(self):
|
332
|
+
"""Get the final configuration for this module."""
|
333
|
+
return self
|
334
|
+
|
210
335
|
@classmethod
|
211
336
|
def get_current_config(cls) -> 'PaymentsConfig':
|
212
337
|
"""
|
@@ -220,3 +345,12 @@ class PaymentsConfig(BaseModel):
|
|
220
345
|
return PaymentsConfigManager.get_payments_config_safe()
|
221
346
|
except Exception:
|
222
347
|
return cls()
|
348
|
+
|
349
|
+
|
350
|
+
# Export all payment configuration classes
|
351
|
+
__all__ = [
|
352
|
+
"BaseProviderConfig",
|
353
|
+
"NowPaymentsProviderConfig",
|
354
|
+
"ProviderAPIKeysConfig",
|
355
|
+
"PaymentsConfig",
|
356
|
+
]
|