django-cfg 1.4.120__py3-none-any.whl → 1.5.2__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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +8 -4
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
- django_cfg/apps/dashboard/TRANSACTION_FIX.md +73 -0
- django_cfg/apps/dashboard/serializers/__init__.py +0 -12
- django_cfg/apps/dashboard/serializers/activity.py +1 -1
- django_cfg/apps/dashboard/services/__init__.py +0 -2
- django_cfg/apps/dashboard/services/charts_service.py +4 -3
- django_cfg/apps/dashboard/services/statistics_service.py +11 -2
- django_cfg/apps/dashboard/services/system_health_service.py +64 -106
- django_cfg/apps/dashboard/urls.py +0 -2
- django_cfg/apps/dashboard/views/__init__.py +0 -2
- django_cfg/apps/dashboard/views/commands_views.py +3 -6
- django_cfg/apps/dashboard/views/overview_views.py +14 -13
- django_cfg/apps/grpc/__init__.py +9 -0
- django_cfg/apps/grpc/admin/__init__.py +11 -0
- django_cfg/apps/{tasks → grpc}/admin/config.py +32 -41
- django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
- django_cfg/apps/grpc/apps.py +28 -0
- django_cfg/apps/grpc/auth/__init__.py +9 -0
- django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
- django_cfg/apps/grpc/interceptors/__init__.py +19 -0
- django_cfg/apps/grpc/interceptors/errors.py +241 -0
- django_cfg/apps/grpc/interceptors/logging.py +270 -0
- django_cfg/apps/grpc/interceptors/metrics.py +306 -0
- django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
- django_cfg/apps/grpc/management/__init__.py +1 -0
- django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
- django_cfg/apps/grpc/managers/__init__.py +10 -0
- django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
- django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
- django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
- django_cfg/apps/grpc/models/__init__.py +9 -0
- django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
- django_cfg/apps/grpc/serializers/__init__.py +23 -0
- django_cfg/apps/grpc/serializers/health.py +18 -0
- django_cfg/apps/grpc/serializers/requests.py +18 -0
- django_cfg/apps/grpc/serializers/services.py +50 -0
- django_cfg/apps/grpc/serializers/stats.py +22 -0
- django_cfg/apps/grpc/services/__init__.py +16 -0
- django_cfg/apps/grpc/services/base.py +375 -0
- django_cfg/apps/grpc/services/discovery.py +415 -0
- django_cfg/apps/grpc/urls.py +23 -0
- django_cfg/apps/grpc/utils/__init__.py +13 -0
- django_cfg/apps/grpc/utils/proto_gen.py +423 -0
- django_cfg/apps/grpc/views/__init__.py +9 -0
- django_cfg/apps/grpc/views/monitoring.py +497 -0
- django_cfg/apps/knowbase/apps.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +7 -9
- django_cfg/apps/maintenance/admin/site_admin.py +5 -4
- django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
- django_cfg/apps/payments/admin/balance_admin.py +26 -36
- django_cfg/apps/payments/admin/payment_admin.py +65 -85
- django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
- django_cfg/apps/rq/__init__.py +9 -0
- django_cfg/apps/rq/apps.py +80 -0
- django_cfg/apps/rq/management/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/rqscheduler.py +31 -0
- django_cfg/apps/rq/management/commands/rqstats.py +33 -0
- django_cfg/apps/rq/management/commands/rqworker.py +31 -0
- django_cfg/apps/rq/management/commands/rqworker_pool.py +27 -0
- django_cfg/apps/rq/serializers/__init__.py +40 -0
- django_cfg/apps/rq/serializers/health.py +60 -0
- django_cfg/apps/rq/serializers/job.py +100 -0
- django_cfg/apps/rq/serializers/queue.py +80 -0
- django_cfg/apps/rq/serializers/schedule.py +178 -0
- django_cfg/apps/rq/serializers/testing.py +139 -0
- django_cfg/apps/rq/serializers/worker.py +58 -0
- django_cfg/apps/rq/services/__init__.py +25 -0
- django_cfg/apps/rq/services/config_helper.py +233 -0
- django_cfg/apps/rq/services/models/README.md +417 -0
- django_cfg/apps/rq/services/models/__init__.py +30 -0
- django_cfg/apps/rq/services/models/event.py +123 -0
- django_cfg/apps/rq/services/models/job.py +99 -0
- django_cfg/apps/rq/services/models/queue.py +92 -0
- django_cfg/apps/rq/services/models/worker.py +104 -0
- django_cfg/apps/rq/services/rq_converters.py +183 -0
- django_cfg/apps/rq/tasks/__init__.py +23 -0
- django_cfg/apps/rq/tasks/demo_tasks.py +284 -0
- django_cfg/apps/rq/urls.py +54 -0
- django_cfg/apps/rq/views/__init__.py +19 -0
- django_cfg/apps/rq/views/jobs.py +882 -0
- django_cfg/apps/rq/views/monitoring.py +248 -0
- django_cfg/apps/rq/views/queues.py +261 -0
- django_cfg/apps/rq/views/schedule.py +400 -0
- django_cfg/apps/rq/views/testing.py +761 -0
- django_cfg/apps/rq/views/workers.py +195 -0
- django_cfg/apps/urls.py +13 -8
- django_cfg/config.py +106 -0
- django_cfg/core/base/config_model.py +16 -26
- django_cfg/core/builders/apps_builder.py +7 -11
- django_cfg/core/generation/integration_generators/__init__.py +3 -6
- django_cfg/core/generation/integration_generators/django_rq.py +80 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
- django_cfg/core/generation/orchestrator.py +15 -15
- django_cfg/core/integration/display/startup.py +6 -20
- django_cfg/mixins/__init__.py +2 -0
- django_cfg/mixins/superadmin_api.py +59 -0
- django_cfg/models/__init__.py +3 -3
- django_cfg/models/api/grpc/__init__.py +59 -0
- django_cfg/models/api/grpc/config.py +364 -0
- django_cfg/models/django/__init__.py +3 -3
- django_cfg/models/django/django_rq.py +621 -0
- django_cfg/models/django/revolution_legacy.py +1 -1
- django_cfg/modules/base.py +19 -6
- django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
- django_cfg/modules/django_admin/config/background_task_config.py +4 -4
- django_cfg/modules/django_admin/utils/__init__.py +41 -3
- django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
- django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
- django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
- django_cfg/modules/django_admin/utils/html/badges.py +47 -0
- django_cfg/modules/django_admin/utils/html/base.py +167 -0
- django_cfg/modules/django_admin/utils/html/code.py +87 -0
- django_cfg/modules/django_admin/utils/html/composition.py +205 -0
- django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
- django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
- django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
- django_cfg/modules/django_admin/utils/html/progress.py +127 -0
- django_cfg/modules/django_admin/utils/html_builder.py +55 -408
- django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
- django_cfg/modules/django_unfold/navigation.py +21 -18
- django_cfg/pyproject.toml +4 -6
- django_cfg/registry/core.py +4 -7
- django_cfg/registry/modules.py +6 -0
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/constance/includes/results_list.html +73 -0
- django_cfg/templates/admin/index.html +187 -62
- django_cfg/templatetags/django_cfg.py +61 -1
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/METADATA +12 -4
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/RECORD +140 -96
- django_cfg/apps/dashboard/permissions.py +0 -48
- django_cfg/apps/dashboard/serializers/django_q2.py +0 -50
- django_cfg/apps/dashboard/services/django_q2_service.py +0 -159
- django_cfg/apps/dashboard/views/django_q2_views.py +0 -79
- django_cfg/apps/tasks/__init__.py +0 -64
- django_cfg/apps/tasks/admin/__init__.py +0 -4
- django_cfg/apps/tasks/admin/task_log.py +0 -265
- django_cfg/apps/tasks/apps.py +0 -15
- django_cfg/apps/tasks/filters/__init__.py +0 -10
- django_cfg/apps/tasks/filters/task_log.py +0 -121
- django_cfg/apps/tasks/migrations/0001_initial.py +0 -196
- django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +0 -16
- django_cfg/apps/tasks/models/__init__.py +0 -4
- django_cfg/apps/tasks/models/task_log.py +0 -246
- django_cfg/apps/tasks/serializers/__init__.py +0 -28
- django_cfg/apps/tasks/serializers/task_log.py +0 -249
- django_cfg/apps/tasks/services/__init__.py +0 -10
- django_cfg/apps/tasks/services/client/__init__.py +0 -7
- django_cfg/apps/tasks/services/client/client.py +0 -234
- django_cfg/apps/tasks/services/config_helper.py +0 -63
- django_cfg/apps/tasks/services/sync.py +0 -204
- django_cfg/apps/tasks/urls.py +0 -16
- django_cfg/apps/tasks/views/__init__.py +0 -10
- django_cfg/apps/tasks/views/task_log.py +0 -41
- django_cfg/apps/tasks/views/task_log_base.py +0 -41
- django_cfg/apps/tasks/views/task_log_overview.py +0 -100
- django_cfg/apps/tasks/views/task_log_related.py +0 -41
- django_cfg/apps/tasks/views/task_log_stats.py +0 -91
- django_cfg/apps/tasks/views/task_log_timeline.py +0 -81
- django_cfg/core/generation/integration_generators/django_q2.py +0 -133
- django_cfg/core/generation/integration_generators/tasks.py +0 -88
- django_cfg/models/django/django_q2.py +0 -514
- django_cfg/models/tasks/__init__.py +0 -49
- django_cfg/models/tasks/backends.py +0 -122
- django_cfg/models/tasks/config.py +0 -209
- django_cfg/models/tasks/utils.py +0 -162
- django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
- django_cfg/modules/django_q2/README.md +0 -140
- django_cfg/modules/django_q2/__init__.py +0 -8
- django_cfg/modules/django_q2/apps.py +0 -107
- django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
- django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +0 -74
- /django_cfg/apps/{tasks/migrations → grpc/management/commands}/__init__.py +0 -0
- /django_cfg/{modules/django_q2/management → apps/grpc/migrations}/__init__.py +0 -0
- /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
- /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper functions for accessing Django-RQ configuration from django-cfg.
|
|
3
|
+
|
|
4
|
+
Provides utilities to get RQ config and check if RQ is enabled.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from django_cfg.modules.django_logging import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger("rq.config")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_rq_config() -> Optional["DjangoRQConfig"]:
|
|
15
|
+
"""
|
|
16
|
+
Get Django-RQ configuration from django-cfg.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
DjangoRQConfig instance or None if not configured
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
>>> config = get_rq_config()
|
|
23
|
+
>>> if config and config.enabled:
|
|
24
|
+
>>> print(config.queues)
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
from django_cfg.core.config import get_current_config
|
|
28
|
+
from django_cfg.models.django.django_rq import DjangoRQConfig
|
|
29
|
+
|
|
30
|
+
config = get_current_config()
|
|
31
|
+
if not config:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
django_rq = getattr(config, 'django_rq', None)
|
|
35
|
+
|
|
36
|
+
# Type validation
|
|
37
|
+
if django_rq and isinstance(django_rq, DjangoRQConfig):
|
|
38
|
+
return django_rq
|
|
39
|
+
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.debug(f"Failed to get RQ config: {e}")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_rq_enabled() -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Check if Django-RQ is enabled in django-cfg.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
True if RQ is enabled, False otherwise
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> if is_rq_enabled():
|
|
56
|
+
>>> from django_rq import enqueue
|
|
57
|
+
>>> enqueue(my_task)
|
|
58
|
+
"""
|
|
59
|
+
config = get_rq_config()
|
|
60
|
+
if not config:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
return getattr(config, 'enabled', False)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_queue_names() -> list:
|
|
67
|
+
"""
|
|
68
|
+
Get list of configured queue names.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of queue names from config
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> queues = get_queue_names()
|
|
75
|
+
>>> print(queues) # ['default', 'high', 'low']
|
|
76
|
+
"""
|
|
77
|
+
config = get_rq_config()
|
|
78
|
+
if not config:
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
queues = getattr(config, 'queues', {})
|
|
82
|
+
if isinstance(queues, dict):
|
|
83
|
+
return list(queues.keys())
|
|
84
|
+
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_prometheus_enabled() -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Check if Prometheus metrics export is enabled.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if Prometheus is enabled, False otherwise
|
|
94
|
+
"""
|
|
95
|
+
config = get_rq_config()
|
|
96
|
+
if not config:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
return getattr(config, 'prometheus_enabled', True)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_redis_url() -> Optional[str]:
|
|
103
|
+
"""
|
|
104
|
+
Get Redis URL from django-cfg DjangoConfig.
|
|
105
|
+
|
|
106
|
+
This is the global Redis URL that is automatically used for:
|
|
107
|
+
- RQ queues (if queue.url is not set)
|
|
108
|
+
- RQ scheduler
|
|
109
|
+
- Cache backend
|
|
110
|
+
- Session backend
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Redis URL string (e.g., "redis://localhost:6379/0") or None
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> redis_url = get_redis_url()
|
|
117
|
+
>>> print(redis_url) # redis://localhost:6379/0
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
from django_cfg.core.config import get_current_config
|
|
121
|
+
|
|
122
|
+
config = get_current_config()
|
|
123
|
+
if not config:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
return getattr(config, 'redis_url', None)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.debug(f"Failed to get redis_url: {e}")
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def register_schedules_from_config():
|
|
134
|
+
"""
|
|
135
|
+
Register scheduled jobs from django-cfg config in rq-scheduler.
|
|
136
|
+
|
|
137
|
+
This function should be called on Django startup (from AppConfig.ready()).
|
|
138
|
+
It reads schedules from config.django_rq.schedules and registers them
|
|
139
|
+
in rq-scheduler.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> from django_cfg.apps.rq.services import register_schedules_from_config
|
|
143
|
+
>>> register_schedules_from_config()
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
import django_rq
|
|
147
|
+
from rq_scheduler import Scheduler
|
|
148
|
+
|
|
149
|
+
config = get_rq_config()
|
|
150
|
+
if not config or not config.enabled:
|
|
151
|
+
logger.debug("RQ not enabled, skipping schedule registration")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
schedules = getattr(config, 'schedules', [])
|
|
155
|
+
if not schedules:
|
|
156
|
+
logger.debug("No schedules configured")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
# Get scheduler for default queue
|
|
160
|
+
queue = django_rq.get_queue('default')
|
|
161
|
+
scheduler = Scheduler(queue=queue, connection=queue.connection)
|
|
162
|
+
|
|
163
|
+
logger.info(f"Registering {len(schedules)} scheduled jobs from config...")
|
|
164
|
+
|
|
165
|
+
for schedule_config in schedules:
|
|
166
|
+
try:
|
|
167
|
+
# Import function
|
|
168
|
+
func_path = schedule_config.func
|
|
169
|
+
module_path, func_name = func_path.rsplit('.', 1)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
import importlib
|
|
173
|
+
module = importlib.import_module(module_path)
|
|
174
|
+
func = getattr(module, func_name)
|
|
175
|
+
except (ImportError, AttributeError) as e:
|
|
176
|
+
logger.warning(f"Failed to import function {func_path}: {e}")
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# Get schedule type and register
|
|
180
|
+
if schedule_config.cron:
|
|
181
|
+
scheduler.cron(
|
|
182
|
+
schedule_config.cron,
|
|
183
|
+
func=func,
|
|
184
|
+
args=schedule_config.args,
|
|
185
|
+
kwargs=schedule_config.kwargs,
|
|
186
|
+
queue_name=schedule_config.queue,
|
|
187
|
+
timeout=schedule_config.timeout,
|
|
188
|
+
result_ttl=schedule_config.result_ttl,
|
|
189
|
+
id=schedule_config.job_id,
|
|
190
|
+
repeat=schedule_config.repeat,
|
|
191
|
+
)
|
|
192
|
+
logger.info(f"✓ Registered cron schedule: {func_path} ({schedule_config.cron})")
|
|
193
|
+
|
|
194
|
+
elif schedule_config.interval:
|
|
195
|
+
from datetime import datetime
|
|
196
|
+
scheduler.schedule(
|
|
197
|
+
scheduled_time=datetime.utcnow(), # Start immediately
|
|
198
|
+
func=func,
|
|
199
|
+
args=schedule_config.args,
|
|
200
|
+
kwargs=schedule_config.kwargs,
|
|
201
|
+
interval=schedule_config.interval,
|
|
202
|
+
queue_name=schedule_config.queue,
|
|
203
|
+
timeout=schedule_config.timeout,
|
|
204
|
+
result_ttl=schedule_config.result_ttl,
|
|
205
|
+
id=schedule_config.job_id,
|
|
206
|
+
repeat=schedule_config.repeat,
|
|
207
|
+
)
|
|
208
|
+
logger.info(f"✓ Registered interval schedule: {func_path} (every {schedule_config.interval}s)")
|
|
209
|
+
|
|
210
|
+
elif schedule_config.scheduled_time:
|
|
211
|
+
from datetime import datetime
|
|
212
|
+
scheduled_dt = datetime.fromisoformat(schedule_config.scheduled_time)
|
|
213
|
+
|
|
214
|
+
scheduler.schedule(
|
|
215
|
+
scheduled_time=scheduled_dt,
|
|
216
|
+
func=func,
|
|
217
|
+
args=schedule_config.args,
|
|
218
|
+
kwargs=schedule_config.kwargs,
|
|
219
|
+
queue_name=schedule_config.queue,
|
|
220
|
+
timeout=schedule_config.timeout,
|
|
221
|
+
result_ttl=schedule_config.result_ttl,
|
|
222
|
+
id=schedule_config.job_id,
|
|
223
|
+
)
|
|
224
|
+
logger.info(f"✓ Registered one-time schedule: {func_path} (at {schedule_config.scheduled_time})")
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Failed to register schedule {schedule_config.func}: {e}")
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
logger.info("Schedule registration completed")
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Failed to register schedules: {e}", exc_info=True)
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# RQ Internal Pydantic Models
|
|
2
|
+
|
|
3
|
+
Внутренние Pydantic модели для бизнес-логики RQ.
|
|
4
|
+
|
|
5
|
+
**ВАЖНО:** Все модели имеют **плоскую структуру** (никаких nested JSON)!
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📁 Структура
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
services/models/
|
|
13
|
+
├── __init__.py # Экспорт всех моделей
|
|
14
|
+
├── job.py # RQJobModel, JobStatus
|
|
15
|
+
├── worker.py # RQWorkerModel, WorkerState
|
|
16
|
+
├── queue.py # RQQueueModel
|
|
17
|
+
├── event.py # Event models для Centrifugo
|
|
18
|
+
└── README.md # Этот файл
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🎯 Назначение
|
|
24
|
+
|
|
25
|
+
### Разделение ответственности:
|
|
26
|
+
|
|
27
|
+
**`/serializers/`** (DRF Serializers):
|
|
28
|
+
- Для API endpoints (views)
|
|
29
|
+
- OpenAPI schema generation
|
|
30
|
+
- HTTP request/response validation
|
|
31
|
+
|
|
32
|
+
**`/services/models/`** (Pydantic Models):
|
|
33
|
+
- Внутренняя бизнес-логика
|
|
34
|
+
- Type safety для сервисов
|
|
35
|
+
- Валидация данных из RQ
|
|
36
|
+
- Computed properties и методы
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 📝 Модели
|
|
41
|
+
|
|
42
|
+
### 1. RQJobModel - Job данные
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from django_cfg.apps.rq.services.models import RQJobModel, JobStatus
|
|
46
|
+
|
|
47
|
+
job = RQJobModel(
|
|
48
|
+
id="abc123",
|
|
49
|
+
func_name="myapp.tasks.send_email",
|
|
50
|
+
queue="default",
|
|
51
|
+
status=JobStatus.FINISHED,
|
|
52
|
+
created_at="2025-01-15T10:00:00Z",
|
|
53
|
+
started_at="2025-01-15T10:00:05Z",
|
|
54
|
+
ended_at="2025-01-15T10:00:10Z",
|
|
55
|
+
worker_name="worker1.12345",
|
|
56
|
+
timeout=180,
|
|
57
|
+
result_ttl=500,
|
|
58
|
+
args_json='["user@example.com", "Hello"]',
|
|
59
|
+
kwargs_json='{"priority": "high"}',
|
|
60
|
+
result_json='{"sent": true}',
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Properties
|
|
64
|
+
print(job.is_success) # True
|
|
65
|
+
print(job.get_duration_seconds()) # 5.0
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Поля (все плоские!):**
|
|
69
|
+
- Базовые: `id`, `func_name`, `queue`, `status`
|
|
70
|
+
- Timestamps: `created_at`, `enqueued_at`, `started_at`, `ended_at` (ISO strings)
|
|
71
|
+
- Worker: `worker_name`
|
|
72
|
+
- Config: `timeout`, `result_ttl`, `failure_ttl`
|
|
73
|
+
- Data: `args_json`, `kwargs_json`, `meta_json`, `result_json` (JSON strings)
|
|
74
|
+
- Dependencies: `dependency_ids` (comma-separated)
|
|
75
|
+
|
|
76
|
+
**Properties:**
|
|
77
|
+
- `is_success` - успешность
|
|
78
|
+
- `is_failed` - провал
|
|
79
|
+
- `is_running` - выполняется
|
|
80
|
+
- `get_duration_seconds()` - длительность
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### 2. RQWorkerModel - Worker данные
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from django_cfg.apps.rq.services.models import RQWorkerModel, WorkerState
|
|
88
|
+
|
|
89
|
+
worker = RQWorkerModel(
|
|
90
|
+
name="worker1.12345",
|
|
91
|
+
state=WorkerState.BUSY,
|
|
92
|
+
queues="default,high,low", # Comma-separated!
|
|
93
|
+
current_job_id="abc123",
|
|
94
|
+
birth="2025-01-15T08:00:00Z",
|
|
95
|
+
last_heartbeat="2025-01-15T10:30:00Z",
|
|
96
|
+
successful_job_count=450,
|
|
97
|
+
failed_job_count=5,
|
|
98
|
+
total_working_time=12500.5,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Properties
|
|
102
|
+
print(worker.is_alive) # True if heartbeat < 60s ago
|
|
103
|
+
print(worker.is_busy) # True
|
|
104
|
+
print(worker.get_uptime_seconds()) # 9000.0
|
|
105
|
+
print(worker.get_queue_list()) # ['default', 'high', 'low']
|
|
106
|
+
print(worker.success_rate) # 98.9%
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Поля (все плоские!):**
|
|
110
|
+
- Базовые: `name`, `state`, `queues` (comma-separated!)
|
|
111
|
+
- Current: `current_job_id`
|
|
112
|
+
- Timestamps: `birth`, `last_heartbeat` (ISO strings)
|
|
113
|
+
- Stats: `successful_job_count`, `failed_job_count`, `total_working_time`
|
|
114
|
+
|
|
115
|
+
**Properties:**
|
|
116
|
+
- `is_alive` - жив ли worker (heartbeat < 60s)
|
|
117
|
+
- `is_busy` / `is_idle` - состояние
|
|
118
|
+
- `get_uptime_seconds()` - время работы
|
|
119
|
+
- `get_queue_list()` - список очередей
|
|
120
|
+
- `total_job_count` - всего задач
|
|
121
|
+
- `success_rate` - процент успеха
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### 3. RQQueueModel - Queue статистика
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from django_cfg.apps.rq.services.models import RQQueueModel
|
|
129
|
+
|
|
130
|
+
queue = RQQueueModel(
|
|
131
|
+
name="default",
|
|
132
|
+
is_async=True,
|
|
133
|
+
count=45,
|
|
134
|
+
queued_jobs=45,
|
|
135
|
+
started_jobs=2,
|
|
136
|
+
finished_jobs=1250,
|
|
137
|
+
failed_jobs=12,
|
|
138
|
+
deferred_jobs=0,
|
|
139
|
+
scheduled_jobs=5,
|
|
140
|
+
workers=3,
|
|
141
|
+
oldest_job_timestamp="2025-01-15T09:15:00Z",
|
|
142
|
+
connection_host="localhost",
|
|
143
|
+
connection_port=6379,
|
|
144
|
+
connection_db=0,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Properties
|
|
148
|
+
print(queue.total_jobs) # 1314
|
|
149
|
+
print(queue.completed_jobs) # 1262
|
|
150
|
+
print(queue.failure_rate) # 0.95%
|
|
151
|
+
print(queue.is_empty) # False
|
|
152
|
+
print(queue.has_workers) # True
|
|
153
|
+
print(queue.is_healthy) # True
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Поля (все плоские!):**
|
|
157
|
+
- Базовые: `name`, `is_async`, `count`
|
|
158
|
+
- Job counts: `queued_jobs`, `started_jobs`, `finished_jobs`, `failed_jobs`, `deferred_jobs`, `scheduled_jobs`
|
|
159
|
+
- Workers: `workers`
|
|
160
|
+
- Metadata: `oldest_job_timestamp` (ISO string)
|
|
161
|
+
- Connection: `connection_host`, `connection_port`, `connection_db` (flat!)
|
|
162
|
+
|
|
163
|
+
**Properties:**
|
|
164
|
+
- `total_jobs` - всего задач
|
|
165
|
+
- `completed_jobs` - завершенных
|
|
166
|
+
- `failure_rate` - процент провалов
|
|
167
|
+
- `is_empty` - пустая ли очередь
|
|
168
|
+
- `has_workers` - есть ли workers
|
|
169
|
+
- `is_healthy` - здорова ли очередь
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### 4. Event Models - для Centrifugo
|
|
174
|
+
|
|
175
|
+
#### JobEventModel
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from django_cfg.apps.rq.services.models import JobEventModel, EventType
|
|
179
|
+
|
|
180
|
+
event = JobEventModel(
|
|
181
|
+
event_type=EventType.JOB_FINISHED,
|
|
182
|
+
timestamp="2025-01-15T10:00:10Z",
|
|
183
|
+
job_id="abc123",
|
|
184
|
+
queue="default",
|
|
185
|
+
func_name="myapp.tasks.send_email",
|
|
186
|
+
status="finished",
|
|
187
|
+
worker_name="worker1.12345",
|
|
188
|
+
result_json='{"sent": true}',
|
|
189
|
+
duration_seconds=5.0,
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Channel:** `rq:jobs`
|
|
194
|
+
|
|
195
|
+
**Поля (все плоские!):**
|
|
196
|
+
- Event: `event_type`, `timestamp`
|
|
197
|
+
- Job: `job_id`, `queue`, `func_name`
|
|
198
|
+
- Status: `status`, `worker_name`
|
|
199
|
+
- Result: `result_json` (JSON string), `error`
|
|
200
|
+
- Timing: `duration_seconds`
|
|
201
|
+
|
|
202
|
+
#### QueueEventModel
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from django_cfg.apps.rq.services.models import QueueEventModel, EventType
|
|
206
|
+
|
|
207
|
+
event = QueueEventModel(
|
|
208
|
+
event_type=EventType.QUEUE_PURGED,
|
|
209
|
+
timestamp="2025-01-15T10:00:00Z",
|
|
210
|
+
queue="default",
|
|
211
|
+
purged_count=45,
|
|
212
|
+
job_count=0,
|
|
213
|
+
)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Channel:** `rq:queues`
|
|
217
|
+
|
|
218
|
+
**Поля (все плоские!):**
|
|
219
|
+
- Event: `event_type`, `timestamp`
|
|
220
|
+
- Queue: `queue`
|
|
221
|
+
- Data: `purged_count`, `job_count`
|
|
222
|
+
|
|
223
|
+
#### WorkerEventModel
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from django_cfg.apps.rq.services.models import WorkerEventModel, EventType
|
|
227
|
+
|
|
228
|
+
event = WorkerEventModel(
|
|
229
|
+
event_type=EventType.WORKER_STARTED,
|
|
230
|
+
timestamp="2025-01-15T08:00:00Z",
|
|
231
|
+
worker_name="worker1.12345",
|
|
232
|
+
queues="default,high,low", # Comma-separated!
|
|
233
|
+
state="idle",
|
|
234
|
+
successful_job_count=0,
|
|
235
|
+
failed_job_count=0,
|
|
236
|
+
total_working_time=0.0,
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Channel:** `rq:workers`
|
|
241
|
+
|
|
242
|
+
**Поля (все плоские!):**
|
|
243
|
+
- Event: `event_type`, `timestamp`
|
|
244
|
+
- Worker: `worker_name`, `queues` (comma-separated!)
|
|
245
|
+
- State: `state`, `current_job_id`
|
|
246
|
+
- Stats: `successful_job_count`, `failed_job_count`, `total_working_time`
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 💡 Примеры использования
|
|
251
|
+
|
|
252
|
+
### Пример 1: Валидация Job данных
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from rq.job import Job
|
|
256
|
+
from django_cfg.apps.rq.services.models import RQJobModel
|
|
257
|
+
|
|
258
|
+
def validate_job(job: Job) -> RQJobModel:
|
|
259
|
+
"""Validate RQ job with Pydantic."""
|
|
260
|
+
import json
|
|
261
|
+
|
|
262
|
+
return RQJobModel(
|
|
263
|
+
id=job.id,
|
|
264
|
+
func_name=job.func_name,
|
|
265
|
+
queue="default", # or extract from job
|
|
266
|
+
status=job.get_status(),
|
|
267
|
+
created_at=job.created_at.isoformat(),
|
|
268
|
+
started_at=job.started_at.isoformat() if job.started_at else None,
|
|
269
|
+
ended_at=job.ended_at.isoformat() if job.ended_at else None,
|
|
270
|
+
worker_name=job.worker_name,
|
|
271
|
+
timeout=job.timeout,
|
|
272
|
+
result_ttl=job.result_ttl,
|
|
273
|
+
failure_ttl=job.failure_ttl,
|
|
274
|
+
args_json=json.dumps(list(job.args or [])),
|
|
275
|
+
kwargs_json=json.dumps(job.kwargs or {}),
|
|
276
|
+
meta_json=json.dumps(job.meta or {}),
|
|
277
|
+
result_json=json.dumps(job.result) if job.result else None,
|
|
278
|
+
exc_info=job.exc_info,
|
|
279
|
+
dependency_ids=",".join(job._dependency_ids or []),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Usage
|
|
283
|
+
job = Job.fetch("abc123", connection=...)
|
|
284
|
+
validated_job = validate_job(job)
|
|
285
|
+
print(validated_job.is_success)
|
|
286
|
+
print(validated_job.get_duration_seconds())
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Пример 2: Публикация события в Centrifugo
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
from datetime import datetime
|
|
293
|
+
from django_cfg.apps.rq.services.models import JobEventModel, EventType
|
|
294
|
+
|
|
295
|
+
def publish_job_completed(job_model: RQJobModel):
|
|
296
|
+
"""Publish job completion event."""
|
|
297
|
+
from django_cfg.apps.rq.services.centrifugo_publisher import publish_to_channel
|
|
298
|
+
|
|
299
|
+
event = JobEventModel(
|
|
300
|
+
event_type=EventType.JOB_FINISHED,
|
|
301
|
+
timestamp=datetime.now().isoformat(),
|
|
302
|
+
job_id=job_model.id,
|
|
303
|
+
queue=job_model.queue,
|
|
304
|
+
func_name=job_model.func_name,
|
|
305
|
+
status=job_model.status,
|
|
306
|
+
worker_name=job_model.worker_name,
|
|
307
|
+
result_json=job_model.result_json,
|
|
308
|
+
duration_seconds=job_model.get_duration_seconds(),
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Pydantic validates and serializes to flat JSON
|
|
312
|
+
publish_to_channel("rq:jobs", event.model_dump())
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Пример 3: Бизнес-логика с типизацией
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
from typing import List
|
|
319
|
+
from django_cfg.apps.rq.services.models import RQJobModel
|
|
320
|
+
|
|
321
|
+
def calculate_avg_duration(jobs: List[RQJobModel]) -> float:
|
|
322
|
+
"""Calculate average job duration with type safety."""
|
|
323
|
+
durations = [j.get_duration_seconds() for j in jobs if j.get_duration_seconds()]
|
|
324
|
+
|
|
325
|
+
if not durations:
|
|
326
|
+
return 0.0
|
|
327
|
+
|
|
328
|
+
return sum(durations) / len(durations)
|
|
329
|
+
|
|
330
|
+
def get_failed_jobs(jobs: List[RQJobModel]) -> List[RQJobModel]:
|
|
331
|
+
"""Filter failed jobs with type safety."""
|
|
332
|
+
return [j for j in jobs if j.is_failed]
|
|
333
|
+
|
|
334
|
+
def group_by_queue(jobs: List[RQJobModel]) -> dict[str, List[RQJobModel]]:
|
|
335
|
+
"""Group jobs by queue with type safety."""
|
|
336
|
+
result = {}
|
|
337
|
+
for job in jobs:
|
|
338
|
+
if job.queue not in result:
|
|
339
|
+
result[job.queue] = []
|
|
340
|
+
result[job.queue].append(job)
|
|
341
|
+
return result
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## ⚠️ Важные правила
|
|
347
|
+
|
|
348
|
+
### 1. NO NESTED JSON!
|
|
349
|
+
|
|
350
|
+
❌ **НЕПРАВИЛЬНО:**
|
|
351
|
+
```python
|
|
352
|
+
class BadJobModel(BaseModel):
|
|
353
|
+
id: str
|
|
354
|
+
config: JobConfig # NESTED!
|
|
355
|
+
result: dict # NESTED!
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
✅ **ПРАВИЛЬНО:**
|
|
359
|
+
```python
|
|
360
|
+
class GoodJobModel(BaseModel):
|
|
361
|
+
id: str
|
|
362
|
+
config_timeout: int
|
|
363
|
+
config_ttl: int
|
|
364
|
+
result_json: str # JSON string!
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### 2. JSON как строки
|
|
368
|
+
|
|
369
|
+
Для сложных данных используем JSON strings:
|
|
370
|
+
```python
|
|
371
|
+
args_json: str = '["arg1", "arg2"]'
|
|
372
|
+
kwargs_json: str = '{"key": "value"}'
|
|
373
|
+
result_json: str = '{"success": true}'
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### 3. Списки как comma-separated строки
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
queues: str = "default,high,low"
|
|
380
|
+
dependency_ids: str = "id1,id2,id3"
|
|
381
|
+
|
|
382
|
+
# Получение списка:
|
|
383
|
+
queue_list = queues.split(",")
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### 4. Timestamps как ISO strings
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
created_at: str = "2025-01-15T10:00:00Z"
|
|
390
|
+
|
|
391
|
+
# Преобразование:
|
|
392
|
+
dt = datetime.fromisoformat(created_at)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## 🎯 Когда использовать
|
|
398
|
+
|
|
399
|
+
**Используй Pydantic models когда:**
|
|
400
|
+
- Нужна валидация данных из RQ
|
|
401
|
+
- Нужна типизация для IDE/mypy
|
|
402
|
+
- Нужны computed properties
|
|
403
|
+
- Нужна бизнес-логика (расчеты, фильтры)
|
|
404
|
+
- Готовишь данные для Centrifugo
|
|
405
|
+
|
|
406
|
+
**НЕ используй Pydantic models для:**
|
|
407
|
+
- API endpoints (используй DRF Serializers)
|
|
408
|
+
- OpenAPI schema (используй DRF Serializers)
|
|
409
|
+
- HTTP request/response (используй DRF Serializers)
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## 📚 См. также
|
|
414
|
+
|
|
415
|
+
- `/serializers/` - DRF Serializers для API
|
|
416
|
+
- `/services/centrifugo_publisher.py` - публикация событий
|
|
417
|
+
- `/services/config_helper.py` - работа с конфигом
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for internal RQ business logic.
|
|
3
|
+
|
|
4
|
+
These models provide type safety and validation for internal operations,
|
|
5
|
+
separate from DRF serializers used in API views.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .job import RQJobModel, JobStatus
|
|
9
|
+
from .worker import RQWorkerModel, WorkerState
|
|
10
|
+
from .queue import RQQueueModel
|
|
11
|
+
from .event import JobEventModel, QueueEventModel, WorkerEventModel, EventType
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
# Job models
|
|
15
|
+
'RQJobModel',
|
|
16
|
+
'JobStatus',
|
|
17
|
+
|
|
18
|
+
# Worker models
|
|
19
|
+
'RQWorkerModel',
|
|
20
|
+
'WorkerState',
|
|
21
|
+
|
|
22
|
+
# Queue models
|
|
23
|
+
'RQQueueModel',
|
|
24
|
+
|
|
25
|
+
# Event models
|
|
26
|
+
'JobEventModel',
|
|
27
|
+
'QueueEventModel',
|
|
28
|
+
'WorkerEventModel',
|
|
29
|
+
'EventType',
|
|
30
|
+
]
|