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,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
@@ -0,0 +1,92 @@
1
+ """
2
+ Pydantic models for RQ Queues.
3
+
4
+ Internal models for queue validation and business logic.
5
+ NO NESTED JSON - all fields are flat!
6
+ """
7
+
8
+ from typing import Optional
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class RQQueueModel(BaseModel):
14
+ """
15
+ Internal model for RQ Queue statistics.
16
+
17
+ FLAT STRUCTURE - no nested objects!
18
+ All fields are primitive types.
19
+ """
20
+
21
+ # Basic info
22
+ name: str = Field(..., description="Queue name")
23
+ is_async: bool = Field(default=True, description="Queue is in async mode")
24
+
25
+ # Job counts by status
26
+ count: int = Field(default=0, ge=0, description="Total jobs in queue")
27
+ queued_jobs: int = Field(default=0, ge=0, description="Jobs waiting to be processed")
28
+ started_jobs: int = Field(default=0, ge=0, description="Jobs currently being processed")
29
+ finished_jobs: int = Field(default=0, ge=0, description="Completed jobs")
30
+ failed_jobs: int = Field(default=0, ge=0, description="Failed jobs")
31
+ deferred_jobs: int = Field(default=0, ge=0, description="Deferred jobs")
32
+ scheduled_jobs: int = Field(default=0, ge=0, description="Scheduled jobs")
33
+
34
+ # Worker info
35
+ workers: int = Field(default=0, ge=0, description="Number of workers for this queue")
36
+
37
+ # Metadata
38
+ oldest_job_timestamp: Optional[str] = Field(
39
+ None, description="Timestamp of oldest job in queue (ISO 8601)"
40
+ )
41
+
42
+ # Connection info (flat!)
43
+ connection_host: Optional[str] = Field(None, description="Redis host")
44
+ connection_port: Optional[int] = Field(None, description="Redis port")
45
+ connection_db: Optional[int] = Field(None, description="Redis DB number")
46
+
47
+ @property
48
+ def total_jobs(self) -> int:
49
+ """Calculate total jobs across all statuses."""
50
+ return (
51
+ self.queued_jobs
52
+ + self.started_jobs
53
+ + self.finished_jobs
54
+ + self.failed_jobs
55
+ + self.deferred_jobs
56
+ + self.scheduled_jobs
57
+ )
58
+
59
+ @property
60
+ def completed_jobs(self) -> int:
61
+ """Total completed jobs (finished + failed)."""
62
+ return self.finished_jobs + self.failed_jobs
63
+
64
+ @property
65
+ def failure_rate(self) -> float:
66
+ """Calculate failure rate percentage (0-100%)."""
67
+ completed = self.completed_jobs
68
+ if completed == 0:
69
+ return 0.0
70
+ return (self.failed_jobs / completed) * 100
71
+
72
+ @property
73
+ def is_empty(self) -> bool:
74
+ """Check if queue is empty."""
75
+ return self.count == 0
76
+
77
+ @property
78
+ def has_workers(self) -> bool:
79
+ """Check if queue has any workers."""
80
+ return self.workers > 0
81
+
82
+ @property
83
+ def is_healthy(self) -> bool:
84
+ """
85
+ Check if queue is healthy.
86
+
87
+ Queue is healthy if:
88
+ - Has workers
89
+ - Failure rate < 50%
90
+ - Not too many queued jobs (< 1000)
91
+ """
92
+ return self.has_workers and self.failure_rate < 50 and self.queued_jobs < 1000
@@ -0,0 +1,104 @@
1
+ """
2
+ Pydantic models for RQ Workers.
3
+
4
+ Internal models for worker 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 WorkerState(str, Enum):
16
+ """Worker state enumeration."""
17
+
18
+ IDLE = "idle"
19
+ BUSY = "busy"
20
+ SUSPENDED = "suspended"
21
+
22
+
23
+ class RQWorkerModel(BaseModel):
24
+ """
25
+ Internal model for RQ Worker.
26
+
27
+ FLAT STRUCTURE - no nested objects!
28
+ Queues are comma-separated string, timestamps are ISO strings.
29
+ """
30
+
31
+ # Basic info
32
+ name: str = Field(..., description="Worker name/ID")
33
+ state: WorkerState = Field(..., description="Worker state (idle/busy/suspended)")
34
+
35
+ # Queues (comma-separated for flat structure)
36
+ queues: str = Field(..., description="Comma-separated queue names (e.g., 'default,high,low')")
37
+
38
+ # Current job
39
+ current_job_id: Optional[str] = Field(None, description="Current job ID if busy")
40
+
41
+ # Timestamps (as ISO strings)
42
+ birth: str = Field(..., description="Worker start time (ISO 8601)")
43
+ last_heartbeat: str = Field(..., description="Last heartbeat timestamp (ISO 8601)")
44
+
45
+ # Statistics
46
+ successful_job_count: int = Field(default=0, ge=0, description="Total successful jobs")
47
+ failed_job_count: int = Field(default=0, ge=0, description="Total failed jobs")
48
+ total_working_time: float = Field(default=0.0, ge=0.0, description="Total working time in seconds")
49
+
50
+ @property
51
+ def is_alive(self) -> bool:
52
+ """
53
+ Check if worker is alive.
54
+
55
+ Worker is considered alive if heartbeat was within last 60 seconds.
56
+ """
57
+ try:
58
+ last_hb = datetime.fromisoformat(self.last_heartbeat)
59
+ now = datetime.now(last_hb.tzinfo) if last_hb.tzinfo else datetime.now()
60
+ delta = (now - last_hb).total_seconds()
61
+ return delta < 60
62
+ except (ValueError, TypeError):
63
+ return False
64
+
65
+ @property
66
+ def is_busy(self) -> bool:
67
+ """Check if worker is busy."""
68
+ return self.state == WorkerState.BUSY
69
+
70
+ @property
71
+ def is_idle(self) -> bool:
72
+ """Check if worker is idle."""
73
+ return self.state == WorkerState.IDLE
74
+
75
+ def get_uptime_seconds(self) -> Optional[float]:
76
+ """Calculate worker uptime in seconds."""
77
+ try:
78
+ birth_dt = datetime.fromisoformat(self.birth)
79
+ now = datetime.now(birth_dt.tzinfo) if birth_dt.tzinfo else datetime.now()
80
+ return (now - birth_dt).total_seconds()
81
+ except (ValueError, TypeError):
82
+ return None
83
+
84
+ def get_queue_list(self) -> list[str]:
85
+ """Get list of queue names."""
86
+ return [q.strip() for q in self.queues.split(",") if q.strip()]
87
+
88
+ @property
89
+ def total_job_count(self) -> int:
90
+ """Total jobs processed (successful + failed)."""
91
+ return self.successful_job_count + self.failed_job_count
92
+
93
+ @property
94
+ def success_rate(self) -> float:
95
+ """Calculate success rate (0-100%)."""
96
+ total = self.total_job_count
97
+ if total == 0:
98
+ return 0.0
99
+ return (self.successful_job_count / total) * 100
100
+
101
+ class Config:
102
+ """Pydantic config."""
103
+
104
+ use_enum_values = True
@@ -0,0 +1,183 @@
1
+ """
2
+ RQ Object to Pydantic Model Converters.
3
+
4
+ Converts RQ objects (Job, Queue, Worker) to type-safe Pydantic models
5
+ for internal business logic.
6
+ """
7
+
8
+ import json
9
+ from typing import Optional
10
+
11
+ from rq import Queue, Worker
12
+ from rq.job import Job
13
+
14
+ from .models import RQJobModel, RQQueueModel, RQWorkerModel, JobStatus, WorkerState
15
+
16
+
17
+ def job_to_model(job: Job, queue_name: Optional[str] = None) -> RQJobModel:
18
+ """
19
+ Convert RQ Job to Pydantic RQJobModel.
20
+
21
+ Args:
22
+ job: RQ Job instance
23
+ queue_name: Queue name (optional, will try to get from job.origin)
24
+
25
+ Returns:
26
+ Validated RQJobModel instance
27
+ """
28
+ # Get queue name
29
+ if not queue_name:
30
+ queue_name = getattr(job, 'origin', 'unknown')
31
+
32
+ # Map RQ status to JobStatus enum
33
+ rq_status = job.get_status()
34
+ status_map = {
35
+ 'queued': JobStatus.QUEUED,
36
+ 'started': JobStatus.STARTED,
37
+ 'finished': JobStatus.FINISHED,
38
+ 'failed': JobStatus.FAILED,
39
+ 'deferred': JobStatus.DEFERRED,
40
+ 'scheduled': JobStatus.SCHEDULED,
41
+ 'canceled': JobStatus.CANCELED,
42
+ }
43
+ status = status_map.get(rq_status, JobStatus.QUEUED)
44
+
45
+ # Serialize args/kwargs/meta to JSON strings (flat!)
46
+ args_json = json.dumps(list(job.args or []))
47
+ kwargs_json = json.dumps(job.kwargs or {})
48
+ meta_json = json.dumps(job.meta or {})
49
+
50
+ # Serialize result to JSON string if available
51
+ result_json = None
52
+ if job.result is not None:
53
+ try:
54
+ result_json = json.dumps(job.result)
55
+ except (TypeError, ValueError):
56
+ # If result is not JSON serializable, convert to string
57
+ result_json = json.dumps(str(job.result))
58
+
59
+ # Get dependency IDs as comma-separated string
60
+ dependency_ids = ""
61
+ if hasattr(job, '_dependency_ids') and job._dependency_ids:
62
+ dependency_ids = ",".join(job._dependency_ids)
63
+
64
+ return RQJobModel(
65
+ id=job.id,
66
+ func_name=job.func_name or "unknown",
67
+ queue=queue_name,
68
+ status=status,
69
+ created_at=job.created_at.isoformat() if job.created_at else "",
70
+ enqueued_at=job.enqueued_at.isoformat() if job.enqueued_at else None,
71
+ started_at=job.started_at.isoformat() if job.started_at else None,
72
+ ended_at=job.ended_at.isoformat() if job.ended_at else None,
73
+ worker_name=job.worker_name,
74
+ timeout=job.timeout,
75
+ result_ttl=job.result_ttl,
76
+ failure_ttl=job.failure_ttl,
77
+ result_json=result_json,
78
+ exc_info=job.exc_info,
79
+ args_json=args_json,
80
+ kwargs_json=kwargs_json,
81
+ meta_json=meta_json,
82
+ dependency_ids=dependency_ids,
83
+ )
84
+
85
+
86
+ def worker_to_model(worker: Worker) -> RQWorkerModel:
87
+ """
88
+ Convert RQ Worker to Pydantic RQWorkerModel.
89
+
90
+ Args:
91
+ worker: RQ Worker instance
92
+
93
+ Returns:
94
+ Validated RQWorkerModel instance
95
+ """
96
+ # Get worker state
97
+ rq_state = worker.get_state()
98
+ state_map = {
99
+ 'idle': WorkerState.IDLE,
100
+ 'busy': WorkerState.BUSY,
101
+ 'suspended': WorkerState.SUSPENDED,
102
+ }
103
+ state = state_map.get(rq_state, WorkerState.IDLE)
104
+
105
+ # Get queues as comma-separated string (flat!)
106
+ queue_names = [q.name for q in worker.queues]
107
+ queues_str = ",".join(queue_names)
108
+
109
+ # Get current job ID
110
+ current_job_id = worker.get_current_job_id()
111
+
112
+ return RQWorkerModel(
113
+ name=worker.name,
114
+ state=state,
115
+ queues=queues_str,
116
+ current_job_id=current_job_id,
117
+ birth=worker.birth_date.isoformat() if worker.birth_date else "",
118
+ last_heartbeat=worker.last_heartbeat.isoformat() if worker.last_heartbeat else "",
119
+ successful_job_count=worker.successful_job_count,
120
+ failed_job_count=worker.failed_job_count,
121
+ total_working_time=worker.total_working_time,
122
+ )
123
+
124
+
125
+ def queue_to_model(queue: Queue, queue_name: str) -> RQQueueModel:
126
+ """
127
+ Convert RQ Queue to Pydantic RQQueueModel.
128
+
129
+ Args:
130
+ queue: RQ Queue instance
131
+ queue_name: Queue name
132
+
133
+ Returns:
134
+ Validated RQQueueModel instance
135
+ """
136
+ # Get job counts from registries
137
+ queued_jobs = len(queue.get_job_ids())
138
+ started_jobs = len(queue.started_job_registry)
139
+ finished_jobs = len(queue.finished_job_registry)
140
+ failed_jobs = len(queue.failed_job_registry)
141
+ deferred_jobs = len(queue.deferred_job_registry)
142
+ scheduled_jobs = len(queue.scheduled_job_registry)
143
+
144
+ # Get worker count
145
+ workers = Worker.all(queue=queue)
146
+ worker_count = len(workers)
147
+
148
+ # Get oldest job timestamp
149
+ oldest_job_timestamp = None
150
+ if queue.count > 0:
151
+ try:
152
+ oldest_job = queue.get_jobs(0, 1)[0]
153
+ oldest_job_timestamp = oldest_job.created_at.isoformat() if oldest_job.created_at else None
154
+ except (IndexError, AttributeError):
155
+ pass
156
+
157
+ # Get connection info (flat!)
158
+ connection_host = None
159
+ connection_port = None
160
+ connection_db = None
161
+ if hasattr(queue.connection, 'connection_pool'):
162
+ pool = queue.connection.connection_pool
163
+ connection_kwargs = pool.connection_kwargs
164
+ connection_host = connection_kwargs.get('host', 'unknown')
165
+ connection_port = connection_kwargs.get('port', 6379)
166
+ connection_db = connection_kwargs.get('db', 0)
167
+
168
+ return RQQueueModel(
169
+ name=queue_name,
170
+ is_async=queue.is_async,
171
+ count=queue.count,
172
+ queued_jobs=queued_jobs,
173
+ started_jobs=started_jobs,
174
+ finished_jobs=finished_jobs,
175
+ failed_jobs=failed_jobs,
176
+ deferred_jobs=deferred_jobs,
177
+ scheduled_jobs=scheduled_jobs,
178
+ workers=worker_count,
179
+ oldest_job_timestamp=oldest_job_timestamp,
180
+ connection_host=connection_host,
181
+ connection_port=connection_port,
182
+ connection_db=connection_db,
183
+ )
@@ -0,0 +1,23 @@
1
+ """
2
+ Demo and test tasks for RQ testing and simulation.
3
+
4
+ These tasks are used for testing RQ functionality from the frontend.
5
+ """
6
+
7
+ from .demo_tasks import (
8
+ demo_success_task,
9
+ demo_failure_task,
10
+ demo_slow_task,
11
+ demo_progress_task,
12
+ demo_crash_task,
13
+ demo_retry_task,
14
+ )
15
+
16
+ __all__ = [
17
+ 'demo_success_task',
18
+ 'demo_failure_task',
19
+ 'demo_slow_task',
20
+ 'demo_progress_task',
21
+ 'demo_crash_task',
22
+ 'demo_retry_task',
23
+ ]