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.

Files changed (121) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/dashboard/TRANSACTION_FIX.md +73 -0
  3. django_cfg/apps/dashboard/serializers/__init__.py +0 -12
  4. django_cfg/apps/dashboard/serializers/activity.py +1 -1
  5. django_cfg/apps/dashboard/services/__init__.py +0 -2
  6. django_cfg/apps/dashboard/services/charts_service.py +4 -3
  7. django_cfg/apps/dashboard/services/statistics_service.py +11 -2
  8. django_cfg/apps/dashboard/services/system_health_service.py +64 -106
  9. django_cfg/apps/dashboard/urls.py +0 -2
  10. django_cfg/apps/dashboard/views/__init__.py +0 -2
  11. django_cfg/apps/dashboard/views/commands_views.py +3 -6
  12. django_cfg/apps/dashboard/views/overview_views.py +14 -13
  13. django_cfg/apps/knowbase/apps.py +2 -2
  14. django_cfg/apps/maintenance/admin/api_key_admin.py +2 -3
  15. django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
  16. django_cfg/apps/rq/__init__.py +9 -0
  17. django_cfg/apps/rq/apps.py +80 -0
  18. django_cfg/apps/rq/management/__init__.py +1 -0
  19. django_cfg/apps/rq/management/commands/__init__.py +1 -0
  20. django_cfg/apps/rq/management/commands/rqscheduler.py +31 -0
  21. django_cfg/apps/rq/management/commands/rqstats.py +33 -0
  22. django_cfg/apps/rq/management/commands/rqworker.py +31 -0
  23. django_cfg/apps/rq/management/commands/rqworker_pool.py +27 -0
  24. django_cfg/apps/rq/serializers/__init__.py +40 -0
  25. django_cfg/apps/rq/serializers/health.py +60 -0
  26. django_cfg/apps/rq/serializers/job.py +100 -0
  27. django_cfg/apps/rq/serializers/queue.py +80 -0
  28. django_cfg/apps/rq/serializers/schedule.py +178 -0
  29. django_cfg/apps/rq/serializers/testing.py +139 -0
  30. django_cfg/apps/rq/serializers/worker.py +58 -0
  31. django_cfg/apps/rq/services/__init__.py +25 -0
  32. django_cfg/apps/rq/services/config_helper.py +233 -0
  33. django_cfg/apps/rq/services/models/README.md +417 -0
  34. django_cfg/apps/rq/services/models/__init__.py +30 -0
  35. django_cfg/apps/rq/services/models/event.py +123 -0
  36. django_cfg/apps/rq/services/models/job.py +99 -0
  37. django_cfg/apps/rq/services/models/queue.py +92 -0
  38. django_cfg/apps/rq/services/models/worker.py +104 -0
  39. django_cfg/apps/rq/services/rq_converters.py +183 -0
  40. django_cfg/apps/rq/tasks/__init__.py +23 -0
  41. django_cfg/apps/rq/tasks/demo_tasks.py +284 -0
  42. django_cfg/apps/rq/urls.py +54 -0
  43. django_cfg/apps/rq/views/__init__.py +19 -0
  44. django_cfg/apps/rq/views/jobs.py +882 -0
  45. django_cfg/apps/rq/views/monitoring.py +248 -0
  46. django_cfg/apps/rq/views/queues.py +261 -0
  47. django_cfg/apps/rq/views/schedule.py +400 -0
  48. django_cfg/apps/rq/views/testing.py +761 -0
  49. django_cfg/apps/rq/views/workers.py +195 -0
  50. django_cfg/apps/urls.py +6 -7
  51. django_cfg/core/base/config_model.py +10 -26
  52. django_cfg/core/builders/apps_builder.py +4 -11
  53. django_cfg/core/generation/integration_generators/__init__.py +3 -6
  54. django_cfg/core/generation/integration_generators/django_rq.py +80 -0
  55. django_cfg/core/generation/orchestrator.py +9 -19
  56. django_cfg/core/integration/display/startup.py +6 -20
  57. django_cfg/mixins/__init__.py +2 -0
  58. django_cfg/mixins/superadmin_api.py +59 -0
  59. django_cfg/models/__init__.py +3 -3
  60. django_cfg/models/django/__init__.py +3 -3
  61. django_cfg/models/django/django_rq.py +621 -0
  62. django_cfg/models/django/revolution_legacy.py +1 -1
  63. django_cfg/modules/base.py +4 -6
  64. django_cfg/modules/django_admin/config/background_task_config.py +4 -4
  65. django_cfg/modules/django_admin/utils/html/composition.py +9 -2
  66. django_cfg/modules/django_unfold/navigation.py +1 -26
  67. django_cfg/pyproject.toml +4 -4
  68. django_cfg/registry/core.py +4 -7
  69. django_cfg/static/frontend/admin.zip +0 -0
  70. django_cfg/templates/admin/constance/includes/results_list.html +73 -0
  71. django_cfg/templates/admin/index.html +187 -62
  72. django_cfg/templatetags/django_cfg.py +61 -1
  73. {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/METADATA +5 -6
  74. {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/RECORD +77 -82
  75. django_cfg/apps/dashboard/permissions.py +0 -48
  76. django_cfg/apps/dashboard/serializers/django_q2.py +0 -50
  77. django_cfg/apps/dashboard/services/django_q2_service.py +0 -159
  78. django_cfg/apps/dashboard/views/django_q2_views.py +0 -79
  79. django_cfg/apps/tasks/__init__.py +0 -64
  80. django_cfg/apps/tasks/admin/__init__.py +0 -4
  81. django_cfg/apps/tasks/admin/config.py +0 -98
  82. django_cfg/apps/tasks/admin/task_log.py +0 -238
  83. django_cfg/apps/tasks/apps.py +0 -15
  84. django_cfg/apps/tasks/filters/__init__.py +0 -10
  85. django_cfg/apps/tasks/filters/task_log.py +0 -121
  86. django_cfg/apps/tasks/migrations/0001_initial.py +0 -196
  87. django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +0 -16
  88. django_cfg/apps/tasks/migrations/__init__.py +0 -0
  89. django_cfg/apps/tasks/models/__init__.py +0 -4
  90. django_cfg/apps/tasks/models/task_log.py +0 -246
  91. django_cfg/apps/tasks/serializers/__init__.py +0 -28
  92. django_cfg/apps/tasks/serializers/task_log.py +0 -249
  93. django_cfg/apps/tasks/services/__init__.py +0 -10
  94. django_cfg/apps/tasks/services/client/__init__.py +0 -7
  95. django_cfg/apps/tasks/services/client/client.py +0 -234
  96. django_cfg/apps/tasks/services/config_helper.py +0 -63
  97. django_cfg/apps/tasks/services/sync.py +0 -204
  98. django_cfg/apps/tasks/urls.py +0 -16
  99. django_cfg/apps/tasks/views/__init__.py +0 -10
  100. django_cfg/apps/tasks/views/task_log.py +0 -41
  101. django_cfg/apps/tasks/views/task_log_base.py +0 -41
  102. django_cfg/apps/tasks/views/task_log_overview.py +0 -100
  103. django_cfg/apps/tasks/views/task_log_related.py +0 -41
  104. django_cfg/apps/tasks/views/task_log_stats.py +0 -91
  105. django_cfg/apps/tasks/views/task_log_timeline.py +0 -81
  106. django_cfg/core/generation/integration_generators/django_q2.py +0 -133
  107. django_cfg/core/generation/integration_generators/tasks.py +0 -88
  108. django_cfg/models/django/django_q2.py +0 -514
  109. django_cfg/models/tasks/__init__.py +0 -49
  110. django_cfg/models/tasks/backends.py +0 -122
  111. django_cfg/models/tasks/config.py +0 -209
  112. django_cfg/models/tasks/utils.py +0 -162
  113. django_cfg/modules/django_q2/README.md +0 -140
  114. django_cfg/modules/django_q2/__init__.py +0 -8
  115. django_cfg/modules/django_q2/apps.py +0 -107
  116. django_cfg/modules/django_q2/management/__init__.py +0 -0
  117. django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
  118. django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +0 -74
  119. {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/WHEEL +0 -0
  120. {django_cfg-1.5.1.dist-info → django_cfg-1.5.3.dist-info}/entry_points.txt +0 -0
  121. {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