django-cfg 1.5.1__py3-none-any.whl → 1.5.3__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 +1 -1
- 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/knowbase/apps.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +2 -3
- django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
- 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 +6 -7
- django_cfg/core/base/config_model.py +10 -26
- django_cfg/core/builders/apps_builder.py +4 -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/orchestrator.py +9 -19
- 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/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 +4 -6
- django_cfg/modules/django_admin/config/background_task_config.py +4 -4
- django_cfg/modules/django_admin/utils/html/composition.py +9 -2
- django_cfg/modules/django_unfold/navigation.py +1 -26
- django_cfg/pyproject.toml +4 -4
- django_cfg/registry/core.py +4 -7
- 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.5.1.dist-info → django_cfg-1.5.3.dist-info}/METADATA +5 -6
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/RECORD +77 -82
- 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/config.py +0 -98
- django_cfg/apps/tasks/admin/task_log.py +0 -238
- 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/migrations/__init__.py +0 -0
- 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_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/__init__.py +0 -0
- 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-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for RQ Events (Centrifugo WebSocket).
|
|
3
|
+
|
|
4
|
+
Internal models for event validation before publishing to Centrifugo.
|
|
5
|
+
NO NESTED JSON - all fields are flat!
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EventType(str, Enum):
|
|
15
|
+
"""Event type enumeration for WebSocket events."""
|
|
16
|
+
|
|
17
|
+
# Job events
|
|
18
|
+
JOB_QUEUED = "job_queued"
|
|
19
|
+
JOB_STARTED = "job_started"
|
|
20
|
+
JOB_FINISHED = "job_finished"
|
|
21
|
+
JOB_FAILED = "job_failed"
|
|
22
|
+
JOB_CANCELED = "job_canceled"
|
|
23
|
+
JOB_REQUEUED = "job_requeued"
|
|
24
|
+
JOB_DELETED = "job_deleted"
|
|
25
|
+
|
|
26
|
+
# Queue events
|
|
27
|
+
QUEUE_PURGED = "queue_purged"
|
|
28
|
+
QUEUE_EMPTIED = "queue_emptied"
|
|
29
|
+
|
|
30
|
+
# Worker events
|
|
31
|
+
WORKER_STARTED = "worker_started"
|
|
32
|
+
WORKER_STOPPED = "worker_stopped"
|
|
33
|
+
WORKER_HEARTBEAT = "worker_heartbeat"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class JobEventModel(BaseModel):
|
|
37
|
+
"""
|
|
38
|
+
Job event for Centrifugo publishing.
|
|
39
|
+
|
|
40
|
+
FLAT STRUCTURE - no nested objects!
|
|
41
|
+
Published to channel: rq:jobs
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Event meta
|
|
45
|
+
event_type: EventType = Field(..., description="Event type")
|
|
46
|
+
timestamp: str = Field(..., description="Event timestamp (ISO 8601)")
|
|
47
|
+
|
|
48
|
+
# Job info
|
|
49
|
+
job_id: str = Field(..., description="Job ID")
|
|
50
|
+
queue: str = Field(..., description="Queue name")
|
|
51
|
+
func_name: Optional[str] = Field(None, description="Function name")
|
|
52
|
+
|
|
53
|
+
# Status info
|
|
54
|
+
status: Optional[str] = Field(None, description="Job status")
|
|
55
|
+
worker_name: Optional[str] = Field(None, description="Worker name")
|
|
56
|
+
|
|
57
|
+
# Result/Error (as JSON strings for flat structure)
|
|
58
|
+
result_json: Optional[str] = Field(None, description="Job result as JSON string")
|
|
59
|
+
error: Optional[str] = Field(None, description="Error message if failed")
|
|
60
|
+
|
|
61
|
+
# Timing
|
|
62
|
+
duration_seconds: Optional[float] = Field(None, description="Job duration in seconds")
|
|
63
|
+
|
|
64
|
+
class Config:
|
|
65
|
+
"""Pydantic config."""
|
|
66
|
+
|
|
67
|
+
use_enum_values = True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class QueueEventModel(BaseModel):
|
|
71
|
+
"""
|
|
72
|
+
Queue event for Centrifugo publishing.
|
|
73
|
+
|
|
74
|
+
FLAT STRUCTURE - no nested objects!
|
|
75
|
+
Published to channel: rq:queues
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# Event meta
|
|
79
|
+
event_type: EventType = Field(..., description="Event type")
|
|
80
|
+
timestamp: str = Field(..., description="Event timestamp (ISO 8601)")
|
|
81
|
+
|
|
82
|
+
# Queue info
|
|
83
|
+
queue: str = Field(..., description="Queue name")
|
|
84
|
+
|
|
85
|
+
# Event-specific data
|
|
86
|
+
purged_count: Optional[int] = Field(None, description="Number of jobs purged")
|
|
87
|
+
job_count: Optional[int] = Field(None, description="Current job count")
|
|
88
|
+
|
|
89
|
+
class Config:
|
|
90
|
+
"""Pydantic config."""
|
|
91
|
+
|
|
92
|
+
use_enum_values = True
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class WorkerEventModel(BaseModel):
|
|
96
|
+
"""
|
|
97
|
+
Worker event for Centrifugo publishing.
|
|
98
|
+
|
|
99
|
+
FLAT STRUCTURE - no nested objects!
|
|
100
|
+
Published to channel: rq:workers
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# Event meta
|
|
104
|
+
event_type: EventType = Field(..., description="Event type")
|
|
105
|
+
timestamp: str = Field(..., description="Event timestamp (ISO 8601)")
|
|
106
|
+
|
|
107
|
+
# Worker info
|
|
108
|
+
worker_name: str = Field(..., description="Worker name")
|
|
109
|
+
queues: str = Field(..., description="Comma-separated queue names")
|
|
110
|
+
|
|
111
|
+
# State info
|
|
112
|
+
state: Optional[str] = Field(None, description="Worker state (idle/busy/suspended)")
|
|
113
|
+
current_job_id: Optional[str] = Field(None, description="Current job ID if busy")
|
|
114
|
+
|
|
115
|
+
# Stats
|
|
116
|
+
successful_job_count: Optional[int] = Field(None, description="Successful job count")
|
|
117
|
+
failed_job_count: Optional[int] = Field(None, description="Failed job count")
|
|
118
|
+
total_working_time: Optional[float] = Field(None, description="Total working time in seconds")
|
|
119
|
+
|
|
120
|
+
class Config:
|
|
121
|
+
"""Pydantic config."""
|
|
122
|
+
|
|
123
|
+
use_enum_values = True
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for RQ Jobs.
|
|
3
|
+
|
|
4
|
+
Internal models for job validation and business logic.
|
|
5
|
+
NO NESTED JSON - all fields are flat!
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class JobStatus(str, Enum):
|
|
16
|
+
"""Job status enumeration."""
|
|
17
|
+
|
|
18
|
+
QUEUED = "queued"
|
|
19
|
+
STARTED = "started"
|
|
20
|
+
FINISHED = "finished"
|
|
21
|
+
FAILED = "failed"
|
|
22
|
+
DEFERRED = "deferred"
|
|
23
|
+
SCHEDULED = "scheduled"
|
|
24
|
+
CANCELED = "canceled"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RQJobModel(BaseModel):
|
|
28
|
+
"""
|
|
29
|
+
Internal model for RQ Job with validation.
|
|
30
|
+
|
|
31
|
+
FLAT STRUCTURE - no nested objects!
|
|
32
|
+
All timestamps are ISO strings, all complex types are flattened.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Basic info
|
|
36
|
+
id: str = Field(..., description="Job ID")
|
|
37
|
+
func_name: str = Field(..., description="Function name (e.g., 'myapp.tasks.send_email')")
|
|
38
|
+
queue: str = Field(..., description="Queue name")
|
|
39
|
+
|
|
40
|
+
# Status
|
|
41
|
+
status: JobStatus = Field(..., description="Current job status")
|
|
42
|
+
|
|
43
|
+
# Timestamps (as ISO strings for flat JSON)
|
|
44
|
+
created_at: str = Field(..., description="Creation timestamp (ISO 8601)")
|
|
45
|
+
enqueued_at: Optional[str] = Field(None, description="Enqueue timestamp (ISO 8601)")
|
|
46
|
+
started_at: Optional[str] = Field(None, description="Start timestamp (ISO 8601)")
|
|
47
|
+
ended_at: Optional[str] = Field(None, description="End timestamp (ISO 8601)")
|
|
48
|
+
|
|
49
|
+
# Worker info
|
|
50
|
+
worker_name: Optional[str] = Field(None, description="Worker name if job is/was running")
|
|
51
|
+
|
|
52
|
+
# Configuration (flat!)
|
|
53
|
+
timeout: Optional[int] = Field(None, description="Job timeout in seconds")
|
|
54
|
+
result_ttl: Optional[int] = Field(None, description="Result TTL in seconds")
|
|
55
|
+
failure_ttl: Optional[int] = Field(None, description="Failure TTL in seconds")
|
|
56
|
+
|
|
57
|
+
# Result/Error (as strings, not nested!)
|
|
58
|
+
result_json: Optional[str] = Field(None, description="Job result as JSON string")
|
|
59
|
+
exc_info: Optional[str] = Field(None, description="Exception info if failed")
|
|
60
|
+
|
|
61
|
+
# Args/Kwargs (as JSON strings for flat structure)
|
|
62
|
+
args_json: str = Field(default="[]", description="Function args as JSON string")
|
|
63
|
+
kwargs_json: str = Field(default="{}", description="Function kwargs as JSON string")
|
|
64
|
+
meta_json: str = Field(default="{}", description="Job metadata as JSON string")
|
|
65
|
+
|
|
66
|
+
# Dependencies (comma-separated for flat structure)
|
|
67
|
+
dependency_ids: str = Field(default="", description="Comma-separated dependency job IDs")
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_success(self) -> bool:
|
|
71
|
+
"""Check if job succeeded."""
|
|
72
|
+
return self.status == JobStatus.FINISHED
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_failed(self) -> bool:
|
|
76
|
+
"""Check if job failed."""
|
|
77
|
+
return self.status == JobStatus.FAILED
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def is_running(self) -> bool:
|
|
81
|
+
"""Check if job is running."""
|
|
82
|
+
return self.status == JobStatus.STARTED
|
|
83
|
+
|
|
84
|
+
def get_duration_seconds(self) -> Optional[float]:
|
|
85
|
+
"""Calculate job duration in seconds."""
|
|
86
|
+
if not self.started_at or not self.ended_at:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
start = datetime.fromisoformat(self.started_at)
|
|
91
|
+
end = datetime.fromisoformat(self.ended_at)
|
|
92
|
+
return (end - start).total_seconds()
|
|
93
|
+
except (ValueError, TypeError):
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
class Config:
|
|
97
|
+
"""Pydantic config."""
|
|
98
|
+
|
|
99
|
+
use_enum_values = True
|