django-cfg 1.4.120__py3-none-any.whl → 1.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +8 -4
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
- django_cfg/apps/dashboard/TRANSACTION_FIX.md +73 -0
- django_cfg/apps/dashboard/serializers/__init__.py +0 -12
- django_cfg/apps/dashboard/serializers/activity.py +1 -1
- django_cfg/apps/dashboard/services/__init__.py +0 -2
- django_cfg/apps/dashboard/services/charts_service.py +4 -3
- django_cfg/apps/dashboard/services/statistics_service.py +11 -2
- django_cfg/apps/dashboard/services/system_health_service.py +64 -106
- django_cfg/apps/dashboard/urls.py +0 -2
- django_cfg/apps/dashboard/views/__init__.py +0 -2
- django_cfg/apps/dashboard/views/commands_views.py +3 -6
- django_cfg/apps/dashboard/views/overview_views.py +14 -13
- django_cfg/apps/grpc/__init__.py +9 -0
- django_cfg/apps/grpc/admin/__init__.py +11 -0
- django_cfg/apps/{tasks → grpc}/admin/config.py +32 -41
- django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
- django_cfg/apps/grpc/apps.py +28 -0
- django_cfg/apps/grpc/auth/__init__.py +9 -0
- django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
- django_cfg/apps/grpc/interceptors/__init__.py +19 -0
- django_cfg/apps/grpc/interceptors/errors.py +241 -0
- django_cfg/apps/grpc/interceptors/logging.py +270 -0
- django_cfg/apps/grpc/interceptors/metrics.py +306 -0
- django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
- django_cfg/apps/grpc/management/__init__.py +1 -0
- django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
- django_cfg/apps/grpc/managers/__init__.py +10 -0
- django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
- django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
- django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
- django_cfg/apps/grpc/models/__init__.py +9 -0
- django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
- django_cfg/apps/grpc/serializers/__init__.py +23 -0
- django_cfg/apps/grpc/serializers/health.py +18 -0
- django_cfg/apps/grpc/serializers/requests.py +18 -0
- django_cfg/apps/grpc/serializers/services.py +50 -0
- django_cfg/apps/grpc/serializers/stats.py +22 -0
- django_cfg/apps/grpc/services/__init__.py +16 -0
- django_cfg/apps/grpc/services/base.py +375 -0
- django_cfg/apps/grpc/services/discovery.py +415 -0
- django_cfg/apps/grpc/urls.py +23 -0
- django_cfg/apps/grpc/utils/__init__.py +13 -0
- django_cfg/apps/grpc/utils/proto_gen.py +423 -0
- django_cfg/apps/grpc/views/__init__.py +9 -0
- django_cfg/apps/grpc/views/monitoring.py +497 -0
- django_cfg/apps/knowbase/apps.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +7 -9
- django_cfg/apps/maintenance/admin/site_admin.py +5 -4
- django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
- django_cfg/apps/payments/admin/balance_admin.py +26 -36
- django_cfg/apps/payments/admin/payment_admin.py +65 -85
- django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
- django_cfg/apps/rq/__init__.py +9 -0
- django_cfg/apps/rq/apps.py +80 -0
- django_cfg/apps/rq/management/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/rqscheduler.py +31 -0
- django_cfg/apps/rq/management/commands/rqstats.py +33 -0
- django_cfg/apps/rq/management/commands/rqworker.py +31 -0
- django_cfg/apps/rq/management/commands/rqworker_pool.py +27 -0
- django_cfg/apps/rq/serializers/__init__.py +40 -0
- django_cfg/apps/rq/serializers/health.py +60 -0
- django_cfg/apps/rq/serializers/job.py +100 -0
- django_cfg/apps/rq/serializers/queue.py +80 -0
- django_cfg/apps/rq/serializers/schedule.py +178 -0
- django_cfg/apps/rq/serializers/testing.py +139 -0
- django_cfg/apps/rq/serializers/worker.py +58 -0
- django_cfg/apps/rq/services/__init__.py +25 -0
- django_cfg/apps/rq/services/config_helper.py +233 -0
- django_cfg/apps/rq/services/models/README.md +417 -0
- django_cfg/apps/rq/services/models/__init__.py +30 -0
- django_cfg/apps/rq/services/models/event.py +123 -0
- django_cfg/apps/rq/services/models/job.py +99 -0
- django_cfg/apps/rq/services/models/queue.py +92 -0
- django_cfg/apps/rq/services/models/worker.py +104 -0
- django_cfg/apps/rq/services/rq_converters.py +183 -0
- django_cfg/apps/rq/tasks/__init__.py +23 -0
- django_cfg/apps/rq/tasks/demo_tasks.py +284 -0
- django_cfg/apps/rq/urls.py +54 -0
- django_cfg/apps/rq/views/__init__.py +19 -0
- django_cfg/apps/rq/views/jobs.py +882 -0
- django_cfg/apps/rq/views/monitoring.py +248 -0
- django_cfg/apps/rq/views/queues.py +261 -0
- django_cfg/apps/rq/views/schedule.py +400 -0
- django_cfg/apps/rq/views/testing.py +761 -0
- django_cfg/apps/rq/views/workers.py +195 -0
- django_cfg/apps/urls.py +13 -8
- django_cfg/config.py +106 -0
- django_cfg/core/base/config_model.py +16 -26
- django_cfg/core/builders/apps_builder.py +7 -11
- django_cfg/core/generation/integration_generators/__init__.py +3 -6
- django_cfg/core/generation/integration_generators/django_rq.py +80 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
- django_cfg/core/generation/orchestrator.py +15 -15
- django_cfg/core/integration/display/startup.py +6 -20
- django_cfg/mixins/__init__.py +2 -0
- django_cfg/mixins/superadmin_api.py +59 -0
- django_cfg/models/__init__.py +3 -3
- django_cfg/models/api/grpc/__init__.py +59 -0
- django_cfg/models/api/grpc/config.py +364 -0
- django_cfg/models/django/__init__.py +3 -3
- django_cfg/models/django/django_rq.py +621 -0
- django_cfg/models/django/revolution_legacy.py +1 -1
- django_cfg/modules/base.py +19 -6
- django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
- django_cfg/modules/django_admin/config/background_task_config.py +4 -4
- django_cfg/modules/django_admin/utils/__init__.py +41 -3
- django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
- django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
- django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
- django_cfg/modules/django_admin/utils/html/badges.py +47 -0
- django_cfg/modules/django_admin/utils/html/base.py +167 -0
- django_cfg/modules/django_admin/utils/html/code.py +87 -0
- django_cfg/modules/django_admin/utils/html/composition.py +205 -0
- django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
- django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
- django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
- django_cfg/modules/django_admin/utils/html/progress.py +127 -0
- django_cfg/modules/django_admin/utils/html_builder.py +55 -408
- django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
- django_cfg/modules/django_unfold/navigation.py +21 -18
- django_cfg/pyproject.toml +4 -6
- django_cfg/registry/core.py +4 -7
- django_cfg/registry/modules.py +6 -0
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/constance/includes/results_list.html +73 -0
- django_cfg/templates/admin/index.html +187 -62
- django_cfg/templatetags/django_cfg.py +61 -1
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/METADATA +12 -4
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/RECORD +140 -96
- django_cfg/apps/dashboard/permissions.py +0 -48
- django_cfg/apps/dashboard/serializers/django_q2.py +0 -50
- django_cfg/apps/dashboard/services/django_q2_service.py +0 -159
- django_cfg/apps/dashboard/views/django_q2_views.py +0 -79
- django_cfg/apps/tasks/__init__.py +0 -64
- django_cfg/apps/tasks/admin/__init__.py +0 -4
- django_cfg/apps/tasks/admin/task_log.py +0 -265
- django_cfg/apps/tasks/apps.py +0 -15
- django_cfg/apps/tasks/filters/__init__.py +0 -10
- django_cfg/apps/tasks/filters/task_log.py +0 -121
- django_cfg/apps/tasks/migrations/0001_initial.py +0 -196
- django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +0 -16
- django_cfg/apps/tasks/models/__init__.py +0 -4
- django_cfg/apps/tasks/models/task_log.py +0 -246
- django_cfg/apps/tasks/serializers/__init__.py +0 -28
- django_cfg/apps/tasks/serializers/task_log.py +0 -249
- django_cfg/apps/tasks/services/__init__.py +0 -10
- django_cfg/apps/tasks/services/client/__init__.py +0 -7
- django_cfg/apps/tasks/services/client/client.py +0 -234
- django_cfg/apps/tasks/services/config_helper.py +0 -63
- django_cfg/apps/tasks/services/sync.py +0 -204
- django_cfg/apps/tasks/urls.py +0 -16
- django_cfg/apps/tasks/views/__init__.py +0 -10
- django_cfg/apps/tasks/views/task_log.py +0 -41
- django_cfg/apps/tasks/views/task_log_base.py +0 -41
- django_cfg/apps/tasks/views/task_log_overview.py +0 -100
- django_cfg/apps/tasks/views/task_log_related.py +0 -41
- django_cfg/apps/tasks/views/task_log_stats.py +0 -91
- django_cfg/apps/tasks/views/task_log_timeline.py +0 -81
- django_cfg/core/generation/integration_generators/django_q2.py +0 -133
- django_cfg/core/generation/integration_generators/tasks.py +0 -88
- django_cfg/models/django/django_q2.py +0 -514
- django_cfg/models/tasks/__init__.py +0 -49
- django_cfg/models/tasks/backends.py +0 -122
- django_cfg/models/tasks/config.py +0 -209
- django_cfg/models/tasks/utils.py +0 -162
- django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
- django_cfg/modules/django_q2/README.md +0 -140
- django_cfg/modules/django_q2/__init__.py +0 -8
- django_cfg/modules/django_q2/apps.py +0 -107
- django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
- django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +0 -74
- /django_cfg/apps/{tasks/migrations → grpc/management/commands}/__init__.py +0 -0
- /django_cfg/{modules/django_q2/management → apps/grpc/migrations}/__init__.py +0 -0
- /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
- /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,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
|
+
]
|