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.

Files changed (182) hide show
  1. django_cfg/__init__.py +8 -4
  2. django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
  3. django_cfg/apps/dashboard/TRANSACTION_FIX.md +73 -0
  4. django_cfg/apps/dashboard/serializers/__init__.py +0 -12
  5. django_cfg/apps/dashboard/serializers/activity.py +1 -1
  6. django_cfg/apps/dashboard/services/__init__.py +0 -2
  7. django_cfg/apps/dashboard/services/charts_service.py +4 -3
  8. django_cfg/apps/dashboard/services/statistics_service.py +11 -2
  9. django_cfg/apps/dashboard/services/system_health_service.py +64 -106
  10. django_cfg/apps/dashboard/urls.py +0 -2
  11. django_cfg/apps/dashboard/views/__init__.py +0 -2
  12. django_cfg/apps/dashboard/views/commands_views.py +3 -6
  13. django_cfg/apps/dashboard/views/overview_views.py +14 -13
  14. django_cfg/apps/grpc/__init__.py +9 -0
  15. django_cfg/apps/grpc/admin/__init__.py +11 -0
  16. django_cfg/apps/{tasks → grpc}/admin/config.py +32 -41
  17. django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
  18. django_cfg/apps/grpc/apps.py +28 -0
  19. django_cfg/apps/grpc/auth/__init__.py +9 -0
  20. django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
  21. django_cfg/apps/grpc/interceptors/__init__.py +19 -0
  22. django_cfg/apps/grpc/interceptors/errors.py +241 -0
  23. django_cfg/apps/grpc/interceptors/logging.py +270 -0
  24. django_cfg/apps/grpc/interceptors/metrics.py +306 -0
  25. django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
  26. django_cfg/apps/grpc/management/__init__.py +1 -0
  27. django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
  28. django_cfg/apps/grpc/managers/__init__.py +10 -0
  29. django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
  30. django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
  31. django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
  32. django_cfg/apps/grpc/models/__init__.py +9 -0
  33. django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
  34. django_cfg/apps/grpc/serializers/__init__.py +23 -0
  35. django_cfg/apps/grpc/serializers/health.py +18 -0
  36. django_cfg/apps/grpc/serializers/requests.py +18 -0
  37. django_cfg/apps/grpc/serializers/services.py +50 -0
  38. django_cfg/apps/grpc/serializers/stats.py +22 -0
  39. django_cfg/apps/grpc/services/__init__.py +16 -0
  40. django_cfg/apps/grpc/services/base.py +375 -0
  41. django_cfg/apps/grpc/services/discovery.py +415 -0
  42. django_cfg/apps/grpc/urls.py +23 -0
  43. django_cfg/apps/grpc/utils/__init__.py +13 -0
  44. django_cfg/apps/grpc/utils/proto_gen.py +423 -0
  45. django_cfg/apps/grpc/views/__init__.py +9 -0
  46. django_cfg/apps/grpc/views/monitoring.py +497 -0
  47. django_cfg/apps/knowbase/apps.py +2 -2
  48. django_cfg/apps/maintenance/admin/api_key_admin.py +7 -9
  49. django_cfg/apps/maintenance/admin/site_admin.py +5 -4
  50. django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
  51. django_cfg/apps/payments/admin/balance_admin.py +26 -36
  52. django_cfg/apps/payments/admin/payment_admin.py +65 -85
  53. django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
  54. django_cfg/apps/rq/__init__.py +9 -0
  55. django_cfg/apps/rq/apps.py +80 -0
  56. django_cfg/apps/rq/management/__init__.py +1 -0
  57. django_cfg/apps/rq/management/commands/__init__.py +1 -0
  58. django_cfg/apps/rq/management/commands/rqscheduler.py +31 -0
  59. django_cfg/apps/rq/management/commands/rqstats.py +33 -0
  60. django_cfg/apps/rq/management/commands/rqworker.py +31 -0
  61. django_cfg/apps/rq/management/commands/rqworker_pool.py +27 -0
  62. django_cfg/apps/rq/serializers/__init__.py +40 -0
  63. django_cfg/apps/rq/serializers/health.py +60 -0
  64. django_cfg/apps/rq/serializers/job.py +100 -0
  65. django_cfg/apps/rq/serializers/queue.py +80 -0
  66. django_cfg/apps/rq/serializers/schedule.py +178 -0
  67. django_cfg/apps/rq/serializers/testing.py +139 -0
  68. django_cfg/apps/rq/serializers/worker.py +58 -0
  69. django_cfg/apps/rq/services/__init__.py +25 -0
  70. django_cfg/apps/rq/services/config_helper.py +233 -0
  71. django_cfg/apps/rq/services/models/README.md +417 -0
  72. django_cfg/apps/rq/services/models/__init__.py +30 -0
  73. django_cfg/apps/rq/services/models/event.py +123 -0
  74. django_cfg/apps/rq/services/models/job.py +99 -0
  75. django_cfg/apps/rq/services/models/queue.py +92 -0
  76. django_cfg/apps/rq/services/models/worker.py +104 -0
  77. django_cfg/apps/rq/services/rq_converters.py +183 -0
  78. django_cfg/apps/rq/tasks/__init__.py +23 -0
  79. django_cfg/apps/rq/tasks/demo_tasks.py +284 -0
  80. django_cfg/apps/rq/urls.py +54 -0
  81. django_cfg/apps/rq/views/__init__.py +19 -0
  82. django_cfg/apps/rq/views/jobs.py +882 -0
  83. django_cfg/apps/rq/views/monitoring.py +248 -0
  84. django_cfg/apps/rq/views/queues.py +261 -0
  85. django_cfg/apps/rq/views/schedule.py +400 -0
  86. django_cfg/apps/rq/views/testing.py +761 -0
  87. django_cfg/apps/rq/views/workers.py +195 -0
  88. django_cfg/apps/urls.py +13 -8
  89. django_cfg/config.py +106 -0
  90. django_cfg/core/base/config_model.py +16 -26
  91. django_cfg/core/builders/apps_builder.py +7 -11
  92. django_cfg/core/generation/integration_generators/__init__.py +3 -6
  93. django_cfg/core/generation/integration_generators/django_rq.py +80 -0
  94. django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
  95. django_cfg/core/generation/orchestrator.py +15 -15
  96. django_cfg/core/integration/display/startup.py +6 -20
  97. django_cfg/mixins/__init__.py +2 -0
  98. django_cfg/mixins/superadmin_api.py +59 -0
  99. django_cfg/models/__init__.py +3 -3
  100. django_cfg/models/api/grpc/__init__.py +59 -0
  101. django_cfg/models/api/grpc/config.py +364 -0
  102. django_cfg/models/django/__init__.py +3 -3
  103. django_cfg/models/django/django_rq.py +621 -0
  104. django_cfg/models/django/revolution_legacy.py +1 -1
  105. django_cfg/modules/base.py +19 -6
  106. django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
  107. django_cfg/modules/django_admin/config/background_task_config.py +4 -4
  108. django_cfg/modules/django_admin/utils/__init__.py +41 -3
  109. django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
  110. django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
  111. django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
  112. django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
  113. django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
  114. django_cfg/modules/django_admin/utils/html/badges.py +47 -0
  115. django_cfg/modules/django_admin/utils/html/base.py +167 -0
  116. django_cfg/modules/django_admin/utils/html/code.py +87 -0
  117. django_cfg/modules/django_admin/utils/html/composition.py +205 -0
  118. django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
  119. django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
  120. django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
  121. django_cfg/modules/django_admin/utils/html/progress.py +127 -0
  122. django_cfg/modules/django_admin/utils/html_builder.py +55 -408
  123. django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
  124. django_cfg/modules/django_unfold/navigation.py +21 -18
  125. django_cfg/pyproject.toml +4 -6
  126. django_cfg/registry/core.py +4 -7
  127. django_cfg/registry/modules.py +6 -0
  128. django_cfg/static/frontend/admin.zip +0 -0
  129. django_cfg/templates/admin/constance/includes/results_list.html +73 -0
  130. django_cfg/templates/admin/index.html +187 -62
  131. django_cfg/templatetags/django_cfg.py +61 -1
  132. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/METADATA +12 -4
  133. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/RECORD +140 -96
  134. django_cfg/apps/dashboard/permissions.py +0 -48
  135. django_cfg/apps/dashboard/serializers/django_q2.py +0 -50
  136. django_cfg/apps/dashboard/services/django_q2_service.py +0 -159
  137. django_cfg/apps/dashboard/views/django_q2_views.py +0 -79
  138. django_cfg/apps/tasks/__init__.py +0 -64
  139. django_cfg/apps/tasks/admin/__init__.py +0 -4
  140. django_cfg/apps/tasks/admin/task_log.py +0 -265
  141. django_cfg/apps/tasks/apps.py +0 -15
  142. django_cfg/apps/tasks/filters/__init__.py +0 -10
  143. django_cfg/apps/tasks/filters/task_log.py +0 -121
  144. django_cfg/apps/tasks/migrations/0001_initial.py +0 -196
  145. django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +0 -16
  146. django_cfg/apps/tasks/models/__init__.py +0 -4
  147. django_cfg/apps/tasks/models/task_log.py +0 -246
  148. django_cfg/apps/tasks/serializers/__init__.py +0 -28
  149. django_cfg/apps/tasks/serializers/task_log.py +0 -249
  150. django_cfg/apps/tasks/services/__init__.py +0 -10
  151. django_cfg/apps/tasks/services/client/__init__.py +0 -7
  152. django_cfg/apps/tasks/services/client/client.py +0 -234
  153. django_cfg/apps/tasks/services/config_helper.py +0 -63
  154. django_cfg/apps/tasks/services/sync.py +0 -204
  155. django_cfg/apps/tasks/urls.py +0 -16
  156. django_cfg/apps/tasks/views/__init__.py +0 -10
  157. django_cfg/apps/tasks/views/task_log.py +0 -41
  158. django_cfg/apps/tasks/views/task_log_base.py +0 -41
  159. django_cfg/apps/tasks/views/task_log_overview.py +0 -100
  160. django_cfg/apps/tasks/views/task_log_related.py +0 -41
  161. django_cfg/apps/tasks/views/task_log_stats.py +0 -91
  162. django_cfg/apps/tasks/views/task_log_timeline.py +0 -81
  163. django_cfg/core/generation/integration_generators/django_q2.py +0 -133
  164. django_cfg/core/generation/integration_generators/tasks.py +0 -88
  165. django_cfg/models/django/django_q2.py +0 -514
  166. django_cfg/models/tasks/__init__.py +0 -49
  167. django_cfg/models/tasks/backends.py +0 -122
  168. django_cfg/models/tasks/config.py +0 -209
  169. django_cfg/models/tasks/utils.py +0 -162
  170. django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
  171. django_cfg/modules/django_q2/README.md +0 -140
  172. django_cfg/modules/django_q2/__init__.py +0 -8
  173. django_cfg/modules/django_q2/apps.py +0 -107
  174. django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
  175. django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +0 -74
  176. /django_cfg/apps/{tasks/migrations → grpc/management/commands}/__init__.py +0 -0
  177. /django_cfg/{modules/django_q2/management → apps/grpc/migrations}/__init__.py +0 -0
  178. /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
  179. /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
  180. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/WHEEL +0 -0
  181. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/entry_points.txt +0 -0
  182. {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
+ ]