django-cfg 1.4.107__py3-none-any.whl → 1.4.108__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/views/profile.py +19 -9
- django_cfg/apps/centrifugo/views/admin_api.py +4 -7
- django_cfg/apps/centrifugo/views/monitoring.py +3 -6
- django_cfg/apps/centrifugo/views/testing_api.py +3 -6
- django_cfg/apps/dashboard/services/system_health_service.py +16 -11
- django_cfg/apps/dashboard/views/activity_views.py +3 -5
- django_cfg/apps/dashboard/views/apizones_views.py +4 -5
- django_cfg/apps/dashboard/views/charts_views.py +4 -5
- django_cfg/apps/dashboard/views/overview_views.py +4 -5
- django_cfg/apps/dashboard/views/statistics_views.py +4 -5
- django_cfg/apps/dashboard/views/system_views.py +4 -5
- django_cfg/apps/knowbase/__init__.py +2 -2
- django_cfg/apps/knowbase/apps.py +2 -8
- django_cfg/apps/knowbase/views/base.py +9 -4
- django_cfg/apps/support/views/api.py +16 -7
- django_cfg/apps/tasks/__init__.py +61 -2
- django_cfg/apps/tasks/admin/__init__.py +3 -10
- django_cfg/apps/tasks/admin/config.py +98 -0
- django_cfg/apps/tasks/admin/task_log.py +265 -0
- django_cfg/apps/tasks/apps.py +7 -9
- django_cfg/apps/tasks/filters/__init__.py +10 -0
- django_cfg/apps/tasks/filters/task_log.py +121 -0
- django_cfg/apps/tasks/migrations/0001_initial.py +196 -0
- django_cfg/apps/tasks/models/__init__.py +4 -0
- django_cfg/apps/tasks/models/task_log.py +246 -0
- django_cfg/apps/tasks/serializers/__init__.py +28 -0
- django_cfg/apps/tasks/serializers/task_log.py +249 -0
- django_cfg/apps/tasks/services/__init__.py +10 -0
- django_cfg/apps/tasks/services/client/__init__.py +7 -0
- django_cfg/apps/tasks/services/client/client.py +234 -0
- django_cfg/apps/tasks/services/config_helper.py +63 -0
- django_cfg/apps/tasks/services/sync.py +204 -0
- django_cfg/apps/tasks/urls.py +7 -13
- django_cfg/apps/tasks/views/__init__.py +4 -10
- django_cfg/apps/tasks/views/task_log.py +41 -0
- django_cfg/apps/tasks/views/task_log_base.py +41 -0
- django_cfg/apps/tasks/views/task_log_overview.py +100 -0
- django_cfg/apps/tasks/views/task_log_related.py +41 -0
- django_cfg/apps/tasks/views/task_log_stats.py +91 -0
- django_cfg/apps/tasks/views/task_log_timeline.py +81 -0
- django_cfg/apps/urls.py +0 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/utils.py +1 -1
- django_cfg/core/base/config_model.py +1 -1
- django_cfg/core/builders/apps_builder.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/tasks.py +14 -18
- django_cfg/core/generation/security_generators/crypto_fields.py +2 -1
- django_cfg/core/integration/display/startup.py +1 -1
- django_cfg/mixins/__init__.py +12 -0
- django_cfg/mixins/admin_api.py +37 -0
- django_cfg/mixins/client_api.py +39 -0
- django_cfg/models/django/constance.py +2 -8
- django_cfg/models/django/crypto_fields.py +13 -48
- django_cfg/models/tasks/__init__.py +8 -10
- django_cfg/models/tasks/backends.py +76 -207
- django_cfg/models/tasks/config.py +20 -127
- django_cfg/models/tasks/utils.py +17 -29
- django_cfg/modules/django_client/management/commands/generate_client.py +13 -1
- django_cfg/modules/django_unfold/navigation.py +121 -22
- django_cfg/pyproject.toml +2 -2
- django_cfg/registry/core.py +1 -1
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.108.dist-info}/METADATA +3 -3
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.108.dist-info}/RECORD +70 -107
- django_cfg/apps/tasks/admin/actions.py +0 -29
- django_cfg/apps/tasks/admin/tasks_admin.py +0 -154
- django_cfg/apps/tasks/api/serializers.py +0 -82
- django_cfg/apps/tasks/api/views.py +0 -571
- django_cfg/apps/tasks/serializers.py +0 -82
- django_cfg/apps/tasks/static/tasks/css/dashboard-alpine.css +0 -299
- django_cfg/apps/tasks/static/tasks/css/dashboard.css +0 -120
- django_cfg/apps/tasks/static/tasks/js/alpine/README.md +0 -47
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/index.js +0 -8
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/management.js +0 -123
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/pagination.js +0 -21
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/tasks.js +0 -101
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/workers.js +0 -59
- django_cfg/apps/tasks/static/tasks/js/alpine/computed.js +0 -35
- django_cfg/apps/tasks/static/tasks/js/alpine/index.js +0 -148
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/index.js +0 -36
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/overview.js +0 -37
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/queues.js +0 -27
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/tasks.js +0 -32
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/workers.js +0 -21
- django_cfg/apps/tasks/static/tasks/js/alpine/state.js +0 -36
- django_cfg/apps/tasks/static/tasks/js/alpine/utils/formatters.js +0 -42
- django_cfg/apps/tasks/static/tasks/js/alpine/utils/helpers.js +0 -68
- django_cfg/apps/tasks/static/tasks/js/dashboard-alpine.js +0 -725
- django_cfg/apps/tasks/tasks/__init__.py +0 -10
- django_cfg/apps/tasks/tasks/demo_tasks.py +0 -127
- django_cfg/apps/tasks/templates/tasks/components/management_actions.html +0 -71
- django_cfg/apps/tasks/templates/tasks/components/overview_content.html +0 -94
- django_cfg/apps/tasks/templates/tasks/components/queues_content.html +0 -44
- django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +0 -45
- django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -151
- django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +0 -61
- django_cfg/apps/tasks/templates/tasks/components/tasks_mjs_integration.html +0 -269
- django_cfg/apps/tasks/templates/tasks/components/workers_content.html +0 -60
- django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -20
- django_cfg/apps/tasks/templates/tasks/pages/dashboard-improved.html +0 -168
- django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +0 -77
- django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +0 -40
- django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +0 -40
- django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +0 -86
- django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +0 -90
- django_cfg/apps/tasks/urls_admin.py +0 -15
- django_cfg/apps/tasks/utils/__init__.py +0 -1
- django_cfg/apps/tasks/utils/simulator.py +0 -353
- django_cfg/apps/tasks/views/api.py +0 -571
- django_cfg/apps/tasks/views/dashboard.py +0 -89
- django_cfg/management/commands/rundramatiq.py +0 -24
- django_cfg/management/commands/rundramatiq_simulator.py +0 -22
- django_cfg/management/commands/task_clear.py +0 -25
- django_cfg/management/commands/task_status.py +0 -24
- django_cfg/modules/django_tasks/__init__.py +0 -29
- django_cfg/modules/django_tasks/dramatiq_setup.py +0 -20
- django_cfg/modules/django_tasks/factory.py +0 -127
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +0 -253
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +0 -436
- django_cfg/modules/django_tasks/management/commands/task_clear.py +0 -226
- django_cfg/modules/django_tasks/management/commands/task_status.py +0 -257
- django_cfg/modules/django_tasks/service.py +0 -281
- django_cfg/modules/django_tasks/settings.py +0 -107
- /django_cfg/{modules/django_tasks/management → apps/tasks/migrations}/__init__.py +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.108.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.108.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.108.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaskLog DRF Serializers.
|
|
3
|
+
|
|
4
|
+
Serializers for TaskLog model with filtering and statistics.
|
|
5
|
+
"""
|
|
6
|
+
from rest_framework import serializers
|
|
7
|
+
from ..models import TaskLog
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskLogSerializer(serializers.ModelSerializer):
|
|
11
|
+
"""
|
|
12
|
+
Basic TaskLog serializer.
|
|
13
|
+
|
|
14
|
+
Used for list views with essential fields only.
|
|
15
|
+
Includes computed properties matching ReArq response format.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
duration_seconds = serializers.SerializerMethodField()
|
|
19
|
+
is_completed = serializers.BooleanField(read_only=True)
|
|
20
|
+
is_successful = serializers.BooleanField(read_only=True)
|
|
21
|
+
is_failed = serializers.BooleanField(read_only=True)
|
|
22
|
+
|
|
23
|
+
class Meta:
|
|
24
|
+
model = TaskLog
|
|
25
|
+
fields = [
|
|
26
|
+
'id',
|
|
27
|
+
'job_id',
|
|
28
|
+
'task_name',
|
|
29
|
+
'queue_name',
|
|
30
|
+
'status',
|
|
31
|
+
'success',
|
|
32
|
+
'duration_ms',
|
|
33
|
+
'duration_seconds',
|
|
34
|
+
'job_retry',
|
|
35
|
+
'job_retries',
|
|
36
|
+
'enqueue_time',
|
|
37
|
+
'expire_time',
|
|
38
|
+
'start_time',
|
|
39
|
+
'finish_time',
|
|
40
|
+
'is_completed',
|
|
41
|
+
'is_successful',
|
|
42
|
+
'is_failed',
|
|
43
|
+
]
|
|
44
|
+
read_only_fields = fields
|
|
45
|
+
|
|
46
|
+
def get_duration_seconds(self, obj) -> float:
|
|
47
|
+
"""Convert duration from ms to seconds."""
|
|
48
|
+
if obj.duration_ms is not None:
|
|
49
|
+
return round(obj.duration_ms / 1000, 2)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TaskLogListSerializer(serializers.ModelSerializer):
|
|
54
|
+
"""
|
|
55
|
+
Compact serializer for list views.
|
|
56
|
+
|
|
57
|
+
Minimal fields for performance, matching ReArq Job list format.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
|
61
|
+
|
|
62
|
+
class Meta:
|
|
63
|
+
model = TaskLog
|
|
64
|
+
fields = [
|
|
65
|
+
'id',
|
|
66
|
+
'job_id',
|
|
67
|
+
'task_name',
|
|
68
|
+
'queue_name',
|
|
69
|
+
'status',
|
|
70
|
+
'status_display',
|
|
71
|
+
'success',
|
|
72
|
+
'job_retries',
|
|
73
|
+
'duration_ms',
|
|
74
|
+
'enqueue_time',
|
|
75
|
+
'start_time',
|
|
76
|
+
'finish_time',
|
|
77
|
+
]
|
|
78
|
+
read_only_fields = fields
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TaskLogDetailSerializer(serializers.ModelSerializer):
|
|
82
|
+
"""
|
|
83
|
+
Detailed TaskLog serializer.
|
|
84
|
+
|
|
85
|
+
Includes all fields including args, kwargs, result, error messages.
|
|
86
|
+
Combines ReArq Job + JobResult data.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
|
90
|
+
duration_seconds = serializers.SerializerMethodField()
|
|
91
|
+
user_display = serializers.SerializerMethodField()
|
|
92
|
+
|
|
93
|
+
class Meta:
|
|
94
|
+
model = TaskLog
|
|
95
|
+
fields = [
|
|
96
|
+
# Job identification
|
|
97
|
+
'id',
|
|
98
|
+
'job_id',
|
|
99
|
+
'task_name',
|
|
100
|
+
'queue_name',
|
|
101
|
+
# Status
|
|
102
|
+
'status',
|
|
103
|
+
'status_display',
|
|
104
|
+
'success',
|
|
105
|
+
# Arguments
|
|
106
|
+
'args',
|
|
107
|
+
'kwargs',
|
|
108
|
+
# Result
|
|
109
|
+
'result',
|
|
110
|
+
'error_message',
|
|
111
|
+
# Performance
|
|
112
|
+
'duration_ms',
|
|
113
|
+
'duration_seconds',
|
|
114
|
+
# Retry info (from ReArq Job)
|
|
115
|
+
'job_retry',
|
|
116
|
+
'job_retries',
|
|
117
|
+
'job_retry_after',
|
|
118
|
+
# Worker
|
|
119
|
+
'worker_id',
|
|
120
|
+
# Timestamps (from ReArq)
|
|
121
|
+
'enqueue_time',
|
|
122
|
+
'expire_time',
|
|
123
|
+
'start_time',
|
|
124
|
+
'finish_time',
|
|
125
|
+
# Django timestamps
|
|
126
|
+
'created_at',
|
|
127
|
+
'updated_at',
|
|
128
|
+
# User
|
|
129
|
+
'user',
|
|
130
|
+
'user_display',
|
|
131
|
+
]
|
|
132
|
+
read_only_fields = fields
|
|
133
|
+
|
|
134
|
+
def get_duration_seconds(self, obj) -> float:
|
|
135
|
+
"""Convert duration from ms to seconds."""
|
|
136
|
+
if obj.duration_ms is not None:
|
|
137
|
+
return round(obj.duration_ms / 1000, 2)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def get_user_display(self, obj) -> str:
|
|
141
|
+
"""Get user display name."""
|
|
142
|
+
if obj.user:
|
|
143
|
+
return f"{obj.user.username} ({obj.user.email})"
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TaskLogStatsSerializer(serializers.Serializer):
|
|
148
|
+
"""
|
|
149
|
+
Statistics serializer for task metrics.
|
|
150
|
+
|
|
151
|
+
Not tied to a model - used for aggregated data.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
total = serializers.IntegerField(help_text="Total number of task executions")
|
|
155
|
+
successful = serializers.IntegerField(help_text="Number of successful executions")
|
|
156
|
+
failed = serializers.IntegerField(help_text="Number of failed executions")
|
|
157
|
+
in_progress = serializers.IntegerField(help_text="Number of tasks currently running")
|
|
158
|
+
success_rate = serializers.FloatField(help_text="Success rate percentage")
|
|
159
|
+
avg_duration_ms = serializers.IntegerField(help_text="Average duration in milliseconds")
|
|
160
|
+
avg_duration_seconds = serializers.FloatField(help_text="Average duration in seconds")
|
|
161
|
+
period_hours = serializers.IntegerField(help_text="Statistics period in hours", required=False)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TasksByQueueSerializer(serializers.Serializer):
|
|
165
|
+
"""
|
|
166
|
+
Tasks count by queue.
|
|
167
|
+
|
|
168
|
+
Used in overview endpoint for tasks_by_queue list.
|
|
169
|
+
"""
|
|
170
|
+
queue_name = serializers.CharField(help_text="Queue name")
|
|
171
|
+
count = serializers.IntegerField(help_text="Number of tasks in this queue")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class TasksByStatusSerializer(serializers.Serializer):
|
|
175
|
+
"""
|
|
176
|
+
Tasks count by status.
|
|
177
|
+
|
|
178
|
+
Used in overview endpoint for tasks_by_status list.
|
|
179
|
+
"""
|
|
180
|
+
status = serializers.CharField(help_text="Task status")
|
|
181
|
+
count = serializers.IntegerField(help_text="Number of tasks with this status")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TaskLogOverviewSerializer(serializers.Serializer):
|
|
185
|
+
"""
|
|
186
|
+
Overview of task system with proper structure.
|
|
187
|
+
|
|
188
|
+
Provides high-level statistics about the entire task system:
|
|
189
|
+
- Total tasks count (all-time)
|
|
190
|
+
- Active queues list
|
|
191
|
+
- Recent failures (last 24h)
|
|
192
|
+
- Tasks distribution by queue (as array)
|
|
193
|
+
- Tasks distribution by status (as array)
|
|
194
|
+
|
|
195
|
+
Used by /cfg/tasks/logs/overview/ endpoint.
|
|
196
|
+
"""
|
|
197
|
+
total_tasks = serializers.IntegerField(help_text="Total number of tasks all-time")
|
|
198
|
+
active_queues = serializers.ListField(
|
|
199
|
+
child=serializers.CharField(),
|
|
200
|
+
help_text="List of active queue names"
|
|
201
|
+
)
|
|
202
|
+
recent_failures = serializers.IntegerField(help_text="Failed tasks in last 24 hours")
|
|
203
|
+
tasks_by_queue = TasksByQueueSerializer(
|
|
204
|
+
many=True,
|
|
205
|
+
help_text="Tasks grouped by queue name"
|
|
206
|
+
)
|
|
207
|
+
tasks_by_status = TasksByStatusSerializer(
|
|
208
|
+
many=True,
|
|
209
|
+
help_text="Tasks grouped by status"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class TaskLogTimelineItemSerializer(serializers.Serializer):
|
|
214
|
+
"""
|
|
215
|
+
Single timeline data point.
|
|
216
|
+
|
|
217
|
+
Represents aggregated task statistics for a specific time period.
|
|
218
|
+
"""
|
|
219
|
+
timestamp = serializers.DateTimeField(help_text="Time bucket start")
|
|
220
|
+
total = serializers.IntegerField(help_text="Total tasks in this period")
|
|
221
|
+
successful = serializers.IntegerField(help_text="Successful tasks")
|
|
222
|
+
failed = serializers.IntegerField(help_text="Failed tasks")
|
|
223
|
+
in_progress = serializers.IntegerField(help_text="Tasks currently in progress", required=False)
|
|
224
|
+
avg_duration_ms = serializers.FloatField(help_text="Average duration in milliseconds", required=False)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class TaskLogTimelineSerializer(serializers.Serializer):
|
|
228
|
+
"""
|
|
229
|
+
Timeline response wrapper.
|
|
230
|
+
|
|
231
|
+
Returns timeline data as array of time-bucketed statistics.
|
|
232
|
+
Used by /cfg/tasks/logs/timeline/ endpoint.
|
|
233
|
+
"""
|
|
234
|
+
period_hours = serializers.IntegerField(help_text="Time period covered in hours")
|
|
235
|
+
interval = serializers.CharField(help_text="Time bucket interval (hour/day)")
|
|
236
|
+
data = TaskLogTimelineItemSerializer(many=True, help_text="Timeline data points")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
__all__ = [
|
|
240
|
+
"TaskLogSerializer",
|
|
241
|
+
"TaskLogListSerializer",
|
|
242
|
+
"TaskLogDetailSerializer",
|
|
243
|
+
"TaskLogStatsSerializer",
|
|
244
|
+
"TasksByQueueSerializer",
|
|
245
|
+
"TasksByStatusSerializer",
|
|
246
|
+
"TaskLogOverviewSerializer",
|
|
247
|
+
"TaskLogTimelineItemSerializer",
|
|
248
|
+
"TaskLogTimelineSerializer",
|
|
249
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Task services module."""
|
|
2
|
+
from .client import ReArqClient, get_rearq_client
|
|
3
|
+
from .config_helper import get_tasks_config, get_tasks_config_or_default
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"ReArqClient",
|
|
7
|
+
"get_rearq_client",
|
|
8
|
+
"get_tasks_config",
|
|
9
|
+
"get_tasks_config_or_default",
|
|
10
|
+
]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ReArq client wrapper for Django-CFG.
|
|
3
|
+
|
|
4
|
+
Provides singleton access to ReArq with django-cfg configuration.
|
|
5
|
+
"""
|
|
6
|
+
from typing import TYPE_CHECKING, Optional
|
|
7
|
+
|
|
8
|
+
from django_cfg.modules.django_logging import get_logger
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from rearq import ReArq
|
|
12
|
+
|
|
13
|
+
logger = get_logger("tasks.client")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ReArqClient:
|
|
17
|
+
"""
|
|
18
|
+
Django wrapper for ReArq client.
|
|
19
|
+
|
|
20
|
+
Provides singleton access to ReArq with django-cfg configuration.
|
|
21
|
+
|
|
22
|
+
Features:
|
|
23
|
+
- Async task queue with Redis backend
|
|
24
|
+
- Job persistence with Tortoise ORM
|
|
25
|
+
- Built-in retry logic
|
|
26
|
+
- Cron task support
|
|
27
|
+
- Job result tracking
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> from django_cfg.apps.tasks import get_rearq_client
|
|
31
|
+
>>>
|
|
32
|
+
>>> client = get_rearq_client()
|
|
33
|
+
>>>
|
|
34
|
+
>>> # Define task
|
|
35
|
+
>>> @client.task(queue="default")
|
|
36
|
+
>>> async def process_data(data_id: str):
|
|
37
|
+
... # Process data
|
|
38
|
+
... return {"status": "done"}
|
|
39
|
+
>>>
|
|
40
|
+
>>> # Execute task
|
|
41
|
+
>>> job = await process_data.delay(data_id="123")
|
|
42
|
+
>>> result = await job.result(timeout=30)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
redis_url: str,
|
|
48
|
+
db_url: str,
|
|
49
|
+
max_jobs: int = 10,
|
|
50
|
+
job_timeout: int = 300,
|
|
51
|
+
job_retry: int = 3,
|
|
52
|
+
job_retry_after: int = 60,
|
|
53
|
+
keep_job_days: int | None = 7,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Initialize ReArq client.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
redis_url: Redis connection URL for task queue
|
|
60
|
+
db_url: Database URL for job persistence (Tortoise ORM)
|
|
61
|
+
max_jobs: Maximum concurrent jobs per worker
|
|
62
|
+
job_timeout: Default job timeout in seconds
|
|
63
|
+
job_retry: Default number of retries for failed jobs
|
|
64
|
+
job_retry_after: Delay in seconds before retrying failed job
|
|
65
|
+
keep_job_days: Days to keep job history (None = forever)
|
|
66
|
+
"""
|
|
67
|
+
self.redis_url = redis_url
|
|
68
|
+
self.db_url = db_url
|
|
69
|
+
self.max_jobs = max_jobs
|
|
70
|
+
self.job_timeout = job_timeout
|
|
71
|
+
self.job_retry = job_retry
|
|
72
|
+
self.job_retry_after = job_retry_after
|
|
73
|
+
self.keep_job_days = keep_job_days
|
|
74
|
+
|
|
75
|
+
# Lazy import ReArq to avoid distutils issues at startup
|
|
76
|
+
from rearq import ReArq
|
|
77
|
+
|
|
78
|
+
# Create ReArq instance
|
|
79
|
+
self.rearq = ReArq(
|
|
80
|
+
redis_url=redis_url,
|
|
81
|
+
job_retry=job_retry,
|
|
82
|
+
job_retry_after=job_retry_after,
|
|
83
|
+
max_jobs=max_jobs,
|
|
84
|
+
job_timeout=job_timeout,
|
|
85
|
+
keep_job_days=keep_job_days,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
logger.info(f"ReArq client initialized: {redis_url}")
|
|
89
|
+
|
|
90
|
+
def task(self, queue: str = "default", **kwargs):
|
|
91
|
+
"""
|
|
92
|
+
Task decorator for defining async tasks.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
queue: Queue name for the task
|
|
96
|
+
**kwargs: Additional task options
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Task decorator
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> @client.task(queue="default")
|
|
103
|
+
>>> async def my_task(arg1: str):
|
|
104
|
+
... return f"Processed {arg1}"
|
|
105
|
+
"""
|
|
106
|
+
return self.rearq.task(queue=queue, **kwargs)
|
|
107
|
+
|
|
108
|
+
def cron_task(self, cron: str, **kwargs):
|
|
109
|
+
"""
|
|
110
|
+
Cron task decorator for scheduled tasks.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
cron: Cron expression (e.g., "0 * * * *" for hourly)
|
|
114
|
+
**kwargs: Additional task options
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Task decorator
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
>>> @client.cron_task(cron="0 0 * * *") # Daily at midnight
|
|
121
|
+
>>> async def daily_cleanup():
|
|
122
|
+
... return "Cleanup complete"
|
|
123
|
+
"""
|
|
124
|
+
return self.rearq.task(cron=cron, **kwargs)
|
|
125
|
+
|
|
126
|
+
async def close(self):
|
|
127
|
+
"""
|
|
128
|
+
Close client connections.
|
|
129
|
+
|
|
130
|
+
Call this when shutting down application to clean up resources.
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
>>> await client.close()
|
|
134
|
+
"""
|
|
135
|
+
await self.rearq.close()
|
|
136
|
+
logger.info("ReArq client closed")
|
|
137
|
+
|
|
138
|
+
def get_connection_info(self) -> dict:
|
|
139
|
+
"""
|
|
140
|
+
Get connection information.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dictionary with connection details
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> info = client.get_connection_info()
|
|
147
|
+
>>> print(info["redis_url"])
|
|
148
|
+
"""
|
|
149
|
+
return {
|
|
150
|
+
"redis_url": self.redis_url,
|
|
151
|
+
"db_url": self.db_url,
|
|
152
|
+
"max_jobs": self.max_jobs,
|
|
153
|
+
"job_timeout": self.job_timeout,
|
|
154
|
+
"job_retry": self.job_retry,
|
|
155
|
+
"job_retry_after": self.job_retry_after,
|
|
156
|
+
"keep_job_days": self.keep_job_days,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ==================== Singleton Pattern ====================
|
|
161
|
+
|
|
162
|
+
_rearq_client: Optional[ReArqClient] = None
|
|
163
|
+
_rearq_client_lock = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_rearq_client(force_new: bool = False) -> ReArqClient:
|
|
167
|
+
"""
|
|
168
|
+
Get global ReArq client instance (singleton).
|
|
169
|
+
|
|
170
|
+
Creates client from Django settings on first call.
|
|
171
|
+
Subsequent calls return the same instance (thread-safe).
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
force_new: Force create new instance (for testing)
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
ReArqClient instance
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> from django_cfg.apps.tasks import get_rearq_client
|
|
181
|
+
>>> client = get_rearq_client()
|
|
182
|
+
>>> @client.task(queue="default")
|
|
183
|
+
>>> async def my_task():
|
|
184
|
+
... pass
|
|
185
|
+
"""
|
|
186
|
+
global _rearq_client, _rearq_client_lock
|
|
187
|
+
|
|
188
|
+
if force_new:
|
|
189
|
+
return _create_client_from_settings()
|
|
190
|
+
|
|
191
|
+
if _rearq_client is None:
|
|
192
|
+
# Thread-safe singleton creation
|
|
193
|
+
import threading
|
|
194
|
+
|
|
195
|
+
if _rearq_client_lock is None:
|
|
196
|
+
_rearq_client_lock = threading.Lock()
|
|
197
|
+
|
|
198
|
+
with _rearq_client_lock:
|
|
199
|
+
if _rearq_client is None:
|
|
200
|
+
_rearq_client = _create_client_from_settings()
|
|
201
|
+
|
|
202
|
+
return _rearq_client
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _create_client_from_settings() -> ReArqClient:
|
|
206
|
+
"""
|
|
207
|
+
Create ReArq client from django-cfg config.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
ReArqClient instance
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
ConfigurationError: If settings not configured
|
|
214
|
+
"""
|
|
215
|
+
from ..config_helper import get_tasks_config_or_default
|
|
216
|
+
|
|
217
|
+
cfg = get_tasks_config_or_default()
|
|
218
|
+
logger.debug(f"Creating ReArq client from config: {cfg.rearq.redis_url}")
|
|
219
|
+
|
|
220
|
+
return ReArqClient(
|
|
221
|
+
redis_url=cfg.rearq.redis_url,
|
|
222
|
+
db_url=cfg.rearq.db_url,
|
|
223
|
+
max_jobs=cfg.rearq.max_jobs,
|
|
224
|
+
job_timeout=cfg.rearq.job_timeout,
|
|
225
|
+
job_retry=cfg.rearq.job_retry,
|
|
226
|
+
job_retry_after=cfg.rearq.job_retry_after,
|
|
227
|
+
keep_job_days=cfg.rearq.keep_job_days,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
__all__ = [
|
|
232
|
+
"ReArqClient",
|
|
233
|
+
"get_rearq_client",
|
|
234
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task configuration helper.
|
|
3
|
+
|
|
4
|
+
Provides access to task configuration from global state.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from django_cfg.modules.django_logging import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger("tasks.config")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_tasks_config():
|
|
14
|
+
"""
|
|
15
|
+
Get tasks configuration from global state.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
TaskConfig instance if found, None otherwise
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> from django_cfg.apps.tasks.services import get_tasks_config
|
|
22
|
+
>>> config = get_tasks_config()
|
|
23
|
+
>>> if config:
|
|
24
|
+
... print(config.rearq.redis_url)
|
|
25
|
+
"""
|
|
26
|
+
from django_cfg.core import get_current_config
|
|
27
|
+
|
|
28
|
+
config = get_current_config()
|
|
29
|
+
|
|
30
|
+
if config and hasattr(config, "tasks") and config.tasks:
|
|
31
|
+
return config.tasks
|
|
32
|
+
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_tasks_config_or_default():
|
|
37
|
+
"""
|
|
38
|
+
Get tasks configuration or return default.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
TaskConfig instance (from global state or default)
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> from django_cfg.apps.tasks.services import get_tasks_config_or_default
|
|
45
|
+
>>> config = get_tasks_config_or_default()
|
|
46
|
+
>>> print(config.rearq.redis_url)
|
|
47
|
+
"""
|
|
48
|
+
config = get_tasks_config()
|
|
49
|
+
|
|
50
|
+
if config:
|
|
51
|
+
return config
|
|
52
|
+
|
|
53
|
+
# Fallback to default
|
|
54
|
+
from django_cfg.models.tasks import TaskConfig
|
|
55
|
+
|
|
56
|
+
logger.warning("Tasks config not found in global state, using default")
|
|
57
|
+
return TaskConfig()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"get_tasks_config",
|
|
62
|
+
"get_tasks_config_or_default",
|
|
63
|
+
]
|