django-simpletask5 0.1.0__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.
Files changed (38) hide show
  1. django_simpletask5/__init__.py +7 -0
  2. django_simpletask5/admin.py +205 -0
  3. django_simpletask5/apps.py +8 -0
  4. django_simpletask5/core/__init__.py +0 -0
  5. django_simpletask5/core/cronjob_registry.py +96 -0
  6. django_simpletask5/core/defaults.py +24 -0
  7. django_simpletask5/core/executor_scanner.py +82 -0
  8. django_simpletask5/core/lock.py +24 -0
  9. django_simpletask5/core/message_queue.py +102 -0
  10. django_simpletask5/core/publisher.py +93 -0
  11. django_simpletask5/core/signals.py +120 -0
  12. django_simpletask5/core/worker_registry.py +134 -0
  13. django_simpletask5/dashboards.py +274 -0
  14. django_simpletask5/executors/__init__.py +0 -0
  15. django_simpletask5/executors/archive.py +24 -0
  16. django_simpletask5/executors/base.py +10 -0
  17. django_simpletask5/executors/bash_script.py +61 -0
  18. django_simpletask5/executors/loader.py +17 -0
  19. django_simpletask5/executors/ping_pong.py +12 -0
  20. django_simpletask5/executors/python_script.py +42 -0
  21. django_simpletask5/executors/retry_timeout.py +32 -0
  22. django_simpletask5/executors/simple_request.py +47 -0
  23. django_simpletask5/executors/status_check.py +40 -0
  24. django_simpletask5/management/__init__.py +0 -0
  25. django_simpletask5/management/commands/__init__.py +0 -0
  26. django_simpletask5/management/commands/django_simpletask_crontab.py +169 -0
  27. django_simpletask5/management/commands/django_simpletask_executor.py +286 -0
  28. django_simpletask5/management/commands/django_simpletask_sync_cronjobs.py +14 -0
  29. django_simpletask5/migrations/0001_initial.py +283 -0
  30. django_simpletask5/migrations/__init__.py +0 -0
  31. django_simpletask5/models.py +274 -0
  32. django_simpletask5/services/__init__.py +0 -0
  33. django_simpletask5/services/archive.py +198 -0
  34. django_simpletask5-0.1.0.dist-info/METADATA +251 -0
  35. django_simpletask5-0.1.0.dist-info/RECORD +38 -0
  36. django_simpletask5-0.1.0.dist-info/WHEEL +5 -0
  37. django_simpletask5-0.1.0.dist-info/licenses/LICENSE +21 -0
  38. django_simpletask5-0.1.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,7 @@
1
+ default_app_config = 'django_simpletask5.apps.DjangoSimpletask5Config'
2
+
3
+ app_requires = [
4
+ 'django_admin_dashboards',
5
+ 'django_safe_fields',
6
+ ]
7
+
@@ -0,0 +1,205 @@
1
+ import json
2
+
3
+ from django import forms
4
+ from django.contrib import admin
5
+ from django.utils.translation import gettext_lazy as _
6
+
7
+
8
+ from django_simpletask5.models import TaskExecution, CronJob, TaskExecutionStat, TaskExecutionArchive
9
+ from django_simpletask5.core.executor_scanner import (
10
+ get_executor_choices,
11
+ get_executor_schema_map,
12
+ )
13
+
14
+
15
+ class ExecutorClassWidget(forms.Select):
16
+ def __init__(self, attrs=None):
17
+ schema_map = get_executor_schema_map()
18
+ choices = [("", "---------")]
19
+ for group_name, items in get_executor_choices():
20
+ group_choices = [(v, v) for v, _ in items]
21
+ choices.append((group_name, group_choices))
22
+ attrs = attrs or {}
23
+ attrs.setdefault("class", "select2-executor")
24
+ attrs.setdefault("style", "width: 600px;")
25
+ attrs["data-schemas"] = json.dumps(schema_map, ensure_ascii=False)
26
+ super().__init__(attrs=attrs, choices=choices)
27
+
28
+
29
+ class CronJobForm(forms.ModelForm):
30
+ executor_class = forms.CharField(
31
+ label=_("Executor class"),
32
+ widget=ExecutorClassWidget(),
33
+ )
34
+ context = forms.CharField(
35
+ label=_("Context"),
36
+ required=False,
37
+ widget=forms.Textarea(
38
+ attrs={
39
+ "rows": 8,
40
+ "style": "width: 600px; font-family: monospace;",
41
+ }
42
+ ),
43
+ help_text=_("Supports JSON or YAML format. YAML is more concise and compatible with JSON standard format."),
44
+ )
45
+
46
+ def __init__(self, *args, **kwargs):
47
+ super().__init__(*args, **kwargs)
48
+ if self.instance and self.instance.pk and self.instance.context:
49
+ try:
50
+ parsed = json.loads(self.instance.context)
51
+ import yaml
52
+
53
+ self.initial["context"] = yaml.dump(
54
+ parsed,
55
+ default_flow_style=False,
56
+ allow_unicode=True,
57
+ sort_keys=False,
58
+ ).strip()
59
+ except Exception:
60
+ pass
61
+
62
+ def clean_context(self):
63
+ raw = self.cleaned_data.get("context")
64
+ if not raw:
65
+ return None
66
+ try:
67
+ parsed = json.loads(raw)
68
+ return json.dumps(parsed, ensure_ascii=False)
69
+ except json.JSONDecodeError:
70
+ pass
71
+ try:
72
+ import yaml
73
+
74
+ parsed = yaml.safe_load(raw)
75
+ if parsed is None:
76
+ return None
77
+ return json.dumps(parsed, ensure_ascii=False)
78
+ except Exception:
79
+ raise forms.ValidationError(_("Invalid parameter format. Please use JSON or YAML format."))
80
+
81
+ class Meta:
82
+ model = CronJob
83
+ fields = "__all__"
84
+
85
+ class Media:
86
+ css = {
87
+ "all": ["admin/css/vendor/select2/select2.min.css"],
88
+ }
89
+ js = [
90
+ "admin/js/vendor/jquery/jquery.min.js",
91
+ "admin/js/vendor/select2/select2.full.min.js",
92
+ "django_simpletask5/js/select2_executor.js",
93
+ "admin/js/jquery.init.js",
94
+ ]
95
+
96
+
97
+ @admin.register(TaskExecution)
98
+ class TaskExecutionAdmin(admin.ModelAdmin):
99
+ list_display = [
100
+ "execution_id",
101
+ "task_id",
102
+ "trigger_event",
103
+ "executor_class",
104
+ "status",
105
+ "retry_count",
106
+ "max_retries",
107
+ "created_at",
108
+ ]
109
+ list_filter = ["status", "trigger_event", "created_at"]
110
+ search_fields = ["execution_id", "task_id", "executor_class"]
111
+ readonly_fields = ["execution_id", "task_id", "created_at", "updated_at"]
112
+ ordering = ["-created_at"]
113
+
114
+
115
+ @admin.register(CronJob)
116
+ class CronJobAdmin(admin.ModelAdmin):
117
+ form = CronJobForm
118
+ list_display = [
119
+ "name",
120
+ "cron_expression",
121
+ "executor_class",
122
+ "is_active",
123
+ "is_modified_by_user",
124
+ "updated_at",
125
+ ]
126
+ list_filter = ["is_active", "is_modified_by_user"]
127
+ search_fields = ["name", "executor_class"]
128
+ actions = [
129
+ "reset_from_code",
130
+ "mark_as_modified",
131
+ "unmark_as_modified",
132
+ "activate_cronjobs",
133
+ "deactivate_cronjobs",
134
+ ]
135
+
136
+ def save_model(self, request, obj, form, change):
137
+ if change:
138
+ obj.is_modified_by_user = True
139
+ super().save_model(request, obj, form, change)
140
+
141
+ def save_related(self, request, form, formsets, change):
142
+ super().save_related(request, form, formsets, change)
143
+
144
+ def reset_from_code(self, request, queryset):
145
+ from django_simpletask5.core.cronjob_registry import _CRONJOB_REGISTRY
146
+
147
+ updated = 0
148
+ for cronjob in queryset:
149
+ if cronjob.name in _CRONJOB_REGISTRY:
150
+ registered = _CRONJOB_REGISTRY[cronjob.name]
151
+ cronjob.cron_expression = registered["cron_expression"]
152
+ cronjob.executor_class = registered["executor_class"]
153
+ cronjob.context = registered.get("context")
154
+ cronjob.description = registered.get("description", "")
155
+ cronjob.is_modified_by_user = False
156
+ cronjob.save()
157
+ updated += 1
158
+ self.message_user(request, f"{updated} cronjob(s) reset from code definitions.")
159
+
160
+ reset_from_code.short_description = _("Reset from code definitions")
161
+
162
+ def mark_as_modified(self, request, queryset):
163
+ updated = queryset.update(is_modified_by_user=True)
164
+ self.message_user(request, f"{updated} cronjob(s) marked as user-modified.")
165
+
166
+ mark_as_modified.short_description = _("Mark as user-modified")
167
+
168
+ def unmark_as_modified(self, request, queryset):
169
+ updated = queryset.update(is_modified_by_user=False)
170
+ self.message_user(request, f"{updated} cronjob(s) unmarked as user-modified.")
171
+
172
+ unmark_as_modified.short_description = _("Unmark as user-modified")
173
+
174
+ def activate_cronjobs(self, request, queryset):
175
+ updated = queryset.update(is_active=True)
176
+ self.message_user(request, f"{updated} cronjob(s) activated.")
177
+
178
+ activate_cronjobs.short_description = _("Activate selected tasks")
179
+
180
+ def deactivate_cronjobs(self, request, queryset):
181
+ updated = queryset.update(is_active=False)
182
+ self.message_user(request, f"{updated} cronjob(s) deactivated.")
183
+
184
+ deactivate_cronjobs.short_description = _("Deactivate selected tasks")
185
+
186
+
187
+ @admin.register(TaskExecutionStat)
188
+ class TaskExecutionStatAdmin(admin.ModelAdmin):
189
+ list_display = [
190
+ "date",
191
+ "trigger_event",
192
+ "total_count",
193
+ "success_count",
194
+ "failed_count",
195
+ "avg_duration_seconds",
196
+ ]
197
+ list_filter = ["date", "trigger_event"]
198
+ ordering = ["-date"]
199
+
200
+
201
+ @admin.register(TaskExecutionArchive)
202
+ class TaskExecutionArchiveAdmin(admin.ModelAdmin):
203
+ list_display = ["archive_date", "created_at"]
204
+ readonly_fields = ["archive_date", "file", "created_at"]
205
+ ordering = ["-archive_date"]
@@ -0,0 +1,8 @@
1
+ from django.apps import AppConfig
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class DjangoSimpletask5Config(AppConfig):
6
+ default_auto_field = 'django.db.models.BigAutoField'
7
+ name = 'django_simpletask5'
8
+ verbose_name = _('Simple Task Manager')
File without changes
@@ -0,0 +1,96 @@
1
+ import json
2
+ import logging
3
+ from datetime import datetime
4
+
5
+ from django.conf import settings
6
+ from django.utils import timezone
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ _CRONJOB_REGISTRY = {}
11
+
12
+
13
+ def register_cronjob(name, cron_expression, executor_class, context=None, description=''):
14
+ _CRONJOB_REGISTRY[name] = {
15
+ 'cron_expression': cron_expression,
16
+ 'executor_class': executor_class,
17
+ 'context': json.dumps(context) if context else None,
18
+ 'description': description,
19
+ }
20
+
21
+
22
+ def _compute_next_run_time(cron_expression, base=None):
23
+ try:
24
+ from croniter import croniter
25
+ except ImportError:
26
+ return None
27
+ if base is None:
28
+ base = timezone.now()
29
+ try:
30
+ cron = croniter(cron_expression, base)
31
+ return cron.get_next(datetime)
32
+ except (ValueError, KeyError):
33
+ return None
34
+
35
+
36
+ def sync_cronjobs():
37
+ from django_simpletask5.models import CronJob
38
+
39
+ if not getattr(settings, 'DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC', True):
40
+ return
41
+
42
+ try:
43
+ CronJob.objects.exists()
44
+ except Exception:
45
+ return
46
+
47
+ for name, config in _CRONJOB_REGISTRY.items():
48
+ try:
49
+ cronjob = CronJob.objects.get(name=name)
50
+ if not cronjob.is_modified_by_user:
51
+ cronjob.cron_expression = config['cron_expression']
52
+ cronjob.executor_class = config['executor_class']
53
+ cronjob.context = config.get('context')
54
+ cronjob.description = config.get('description', '')
55
+ if cronjob.next_run_time is None:
56
+ cronjob.next_run_time = _compute_next_run_time(config['cron_expression'])
57
+ cronjob.save()
58
+ except CronJob.DoesNotExist:
59
+ CronJob.objects.create(
60
+ name=name,
61
+ cron_expression=config['cron_expression'],
62
+ executor_class=config['executor_class'],
63
+ context=config.get('context'),
64
+ description=config.get('description', ''),
65
+ is_modified_by_user=False,
66
+ next_run_time=_compute_next_run_time(config['cron_expression']),
67
+ )
68
+ except Exception:
69
+ logger.exception('Failed to sync cronjob %s', name)
70
+
71
+
72
+ def register_builtin_cronjobs():
73
+ register_cronjob(
74
+ name='simpletask_status_check',
75
+ cron_expression='*/5 * * * *',
76
+ executor_class='django_simpletask5.executors.status_check.StatusCheckExecutor',
77
+ description='Scan long-running pending/running tasks and mark them as timeout',
78
+ )
79
+ register_cronjob(
80
+ name='simpletask_retry_timeout',
81
+ cron_expression='*/10 * * * *',
82
+ executor_class='django_simpletask5.executors.retry_timeout.RetryTimeoutExecutor',
83
+ description='Retry timeout tasks that still have retry attempts remaining',
84
+ )
85
+ register_cronjob(
86
+ name='simpletask_archive',
87
+ cron_expression='0 2 * * *',
88
+ executor_class='django_simpletask5.executors.archive.ArchiveExecutor',
89
+ description='Archive previous day TaskExecution records',
90
+ )
91
+ register_cronjob(
92
+ name='simpletask_ping_pong',
93
+ cron_expression='* * * * *',
94
+ executor_class='django_simpletask5.executors.ping_pong.PingPongExecutor',
95
+ description='Ping pong health check',
96
+ )
@@ -0,0 +1,24 @@
1
+ from django.conf import settings
2
+
3
+ DJANGO_SIMPLETASK_LOCK_TIMEOUT = getattr(settings, 'DJANGO_SIMPLETASK_LOCK_TIMEOUT', 600)
4
+
5
+ DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES = getattr(settings, 'DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES', 3)
6
+ DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS = getattr(settings, 'DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS', 300)
7
+
8
+ DJANGO_SIMPLETASK_UPDATE_FILTER_POLICY = getattr(settings, 'DJANGO_SIMPLETASK_UPDATE_FILTER_POLICY', 'whitelist')
9
+
10
+ DJANGO_SIMPLETASK_ARCHIVE_PATH = getattr(settings, 'DJANGO_SIMPLETASK_ARCHIVE_PATH', 'django_simpletask5_archives')
11
+ DJANGO_SIMPLETASK_ARCHIVE_SALT = getattr(settings, 'DJANGO_SIMPLETASK_ARCHIVE_SALT', 'django-simpletask5-archive')
12
+
13
+ DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC = getattr(settings, 'DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC', True)
14
+
15
+ DJANGO_SIMPLETASK_DEFAULT_QUEUE = getattr(settings, 'DJANGO_SIMPLETASK_DEFAULT_QUEUE', 'django_simpletask5.queue.default')
16
+ DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE = getattr(settings, 'DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE', 'django_simpletask5.queue.high_priority')
17
+
18
+ DJANGO_SIMPLETASK_SCRIPT_WHITELIST = getattr(settings, 'DJANGO_SIMPLETASK_SCRIPT_WHITELIST', [])
19
+
20
+ DJANGO_SIMPLETASK_FIELD_CIPHER_CLASS = getattr(settings, 'DJANGO_SIMPLETASK_FIELD_CIPHER_CLASS', None)
21
+
22
+ DJANGO_SIMPLETASK_RESULT_PASSWORD = getattr(settings, 'DJANGO_SIMPLETASK_RESULT_PASSWORD', None)
23
+ DJANGO_SIMPLETASK_ERROR_MSG_PASSWORD = getattr(settings, 'DJANGO_SIMPLETASK_ERROR_MSG_PASSWORD', None)
24
+ DJANGO_SIMPLETASK_CONTEXT_PASSWORD = getattr(settings, 'DJANGO_SIMPLETASK_CONTEXT_PASSWORD', None)
@@ -0,0 +1,82 @@
1
+ import importlib
2
+ import inspect
3
+ import logging
4
+ import pkgutil
5
+
6
+ from django.apps import apps
7
+
8
+ from django_simpletask5.executors.base import BaseExecutor
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def _scan_module(module, base_path, app_label, verbose_name, executors):
14
+ for name, cls in inspect.getmembers(module, inspect.isclass):
15
+ if cls is BaseExecutor:
16
+ continue
17
+ if not issubclass(cls, BaseExecutor):
18
+ continue
19
+ path = f'{base_path}.{name}'
20
+ schema = []
21
+ try:
22
+ schema = cls.get_parameter_schema()
23
+ except Exception:
24
+ pass
25
+ executors[path] = {
26
+ 'name': name,
27
+ 'path': path,
28
+ 'app_label': app_label,
29
+ 'app_name': verbose_name,
30
+ 'schema': schema,
31
+ }
32
+
33
+
34
+ def _scan_package(package_name, app_label, verbose_name, executors):
35
+ try:
36
+ package = importlib.import_module(package_name)
37
+ except ImportError:
38
+ return
39
+ _scan_module(package, package_name, app_label, verbose_name, executors)
40
+ try:
41
+ for importer, modname, ispkg in pkgutil.iter_modules(package.__path__):
42
+ if modname.startswith('_'):
43
+ continue
44
+ full_name = f'{package_name}.{modname}'
45
+ if ispkg:
46
+ _scan_package(full_name, app_label, verbose_name, executors)
47
+ else:
48
+ try:
49
+ mod = importlib.import_module(full_name)
50
+ _scan_module(mod, full_name, app_label, verbose_name, executors)
51
+ except ImportError:
52
+ continue
53
+ except Exception:
54
+ pass
55
+
56
+
57
+ def discover_executors():
58
+ executors = {}
59
+ for app_config in apps.get_app_configs():
60
+ app_label = app_config.label
61
+ verbose_name = app_config.verbose_name
62
+ _scan_package(f'{app_config.name}.executors', app_label, verbose_name, executors)
63
+ return executors
64
+
65
+
66
+ def get_executor_choices():
67
+ executors = discover_executors()
68
+ groups = {}
69
+ for path, info in executors.items():
70
+ group = groups.setdefault(info['app_name'], [])
71
+ group.append((path, f"{info['name']}"))
72
+ sorted_groups = sorted(groups.items(), key=lambda x: x[0])
73
+ choices = []
74
+ for group_name, items in sorted_groups:
75
+ sorted_items = sorted(items, key=lambda x: x[1])
76
+ choices.append((group_name, sorted_items))
77
+ return choices
78
+
79
+
80
+ def get_executor_schema_map():
81
+ executors = discover_executors()
82
+ return {path: info['schema'] for path, info in executors.items()}
@@ -0,0 +1,24 @@
1
+ import logging
2
+
3
+ from django.conf import settings
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ def get_lock_manager():
9
+ from globallock import GlobalLockManager
10
+ config = getattr(settings, 'DJANGO_SIMPLETASK_LOCK_CONFIG', None)
11
+ if config is None:
12
+ raise RuntimeError(
13
+ 'DJANGO_SIMPLETASK_LOCK_CONFIG is not configured. '
14
+ 'Set it in Django settings, e.g.:\n'
15
+ 'DJANGO_SIMPLETASK_LOCK_CONFIG = {\n'
16
+ ' "global_lock_engine_class": "globallock.redis_global_lock.RedisGlobalLock",\n'
17
+ ' "global_lock_engine_options": {\n'
18
+ ' "host": "127.0.0.1",\n'
19
+ ' "port": 6379,\n'
20
+ ' "db": 0,\n'
21
+ ' },\n'
22
+ '}'
23
+ )
24
+ return GlobalLockManager(config)
@@ -0,0 +1,102 @@
1
+ import logging
2
+ import socket
3
+
4
+ from django.conf import settings
5
+ from kombu import Connection, Exchange, Queue, Producer, Consumer
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def _get_broker_url():
11
+ url = getattr(settings, 'DJANGO_SIMPLETASK_BROKER_URL', None)
12
+ if url:
13
+ return url
14
+ transport = getattr(settings, 'DJANGO_SIMPLETASK_MESSAGE_QUEUE_TYPE', 'memory')
15
+ if transport == 'rabbitmq':
16
+ host = getattr(settings, 'DJANGO_SIMPLETASK_RABBITMQ_HOST', '127.0.0.1')
17
+ port = getattr(settings, 'DJANGO_SIMPLETASK_RABBITMQ_PORT', 5672)
18
+ user = getattr(settings, 'DJANGO_SIMPLETASK_RABBITMQ_USER', 'guest')
19
+ pw = getattr(settings, 'DJANGO_SIMPLETASK_RABBITMQ_PASSWORD', 'guest')
20
+ return f'amqp://{user}:{pw}@{host}:{port}//'
21
+ if transport == 'redis':
22
+ host = getattr(settings, 'DJANGO_SIMPLETASK_REDIS_MQ_HOST', '127.0.0.1')
23
+ port = getattr(settings, 'DJANGO_SIMPLETASK_REDIS_MQ_PORT', 6379)
24
+ db = getattr(settings, 'DJANGO_SIMPLETASK_REDIS_MQ_DB', 0)
25
+ return f'redis://{host}:{port}/{db}'
26
+ return 'memory://'
27
+
28
+
29
+ class KombuQueue:
30
+ """Kombu-based message queue with publish/consume support."""
31
+
32
+ def __init__(self):
33
+ self._exchange = Exchange('django_simpletask5', type='direct', durable=True)
34
+ self._stopped = False
35
+
36
+ def _declare(self, conn, routing_key):
37
+ ch = conn.channel()
38
+ self._exchange.declare(channel=ch)
39
+ queue = Queue(routing_key, self._exchange, routing_key=routing_key, durable=True)
40
+ queue.declare(channel=ch)
41
+ ch.close()
42
+
43
+ def publish(self, routing_key, message):
44
+ with Connection(_get_broker_url()) as conn:
45
+ conn.connect()
46
+ self._declare(conn, routing_key)
47
+ with Producer(conn) as producer:
48
+ producer.publish(
49
+ message,
50
+ exchange=self._exchange,
51
+ routing_key=routing_key,
52
+ serializer='json',
53
+ retry=True,
54
+ )
55
+
56
+ def consume(self, queue_name, callback, auto_ack=True):
57
+ retry_delay = 1
58
+ while not self._stopped:
59
+ try:
60
+ with Connection(_get_broker_url()) as conn:
61
+ conn.connect()
62
+ queue = Queue(queue_name, self._exchange, routing_key=queue_name, durable=True)
63
+
64
+ with Consumer(
65
+ conn,
66
+ queues=[queue],
67
+ callbacks=[lambda body, msg: self._on_message(body, msg, callback, auto_ack)],
68
+ accept=['json'],
69
+ ):
70
+ retry_delay = 1
71
+ while not self._stopped:
72
+ try:
73
+ conn.drain_events(timeout=0.5)
74
+ except socket.timeout:
75
+ continue
76
+ except Exception as e:
77
+ if not self._stopped:
78
+ logger.debug('Consumer connection lost: %s, reconnecting in %ss...', e, retry_delay)
79
+ import time as _time
80
+ _time.sleep(retry_delay)
81
+ retry_delay = min(retry_delay * 2, 10)
82
+
83
+ @staticmethod
84
+ def _on_message(body, message, callback, auto_ack):
85
+ try:
86
+ result = callback(body)
87
+ if result is False:
88
+ message.reject(requeue=True)
89
+ return
90
+ except Exception:
91
+ logger.exception('Error in message callback')
92
+ message.reject(requeue=True)
93
+ return
94
+ if auto_ack:
95
+ message.ack()
96
+
97
+ def stop(self):
98
+ self._stopped = True
99
+
100
+
101
+ def get_message_queue():
102
+ return KombuQueue()
@@ -0,0 +1,93 @@
1
+ import json
2
+ import logging
3
+
4
+
5
+ from django_simpletask5.models import TaskExecution
6
+ from django_simpletask5.core.defaults import DJANGO_SIMPLETASK_DEFAULT_QUEUE
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ def create_and_publish(task, trigger_event, extra_context=None):
11
+ executor_class_path = task.get_executor_class_path(event=trigger_event)
12
+ queue_name = task.get_queue_name(event=trigger_event)
13
+
14
+ context = {
15
+ 'task_id': str(task.task_id),
16
+ 'trigger_event': trigger_event,
17
+ 'data': _model_to_dict(task),
18
+ }
19
+ if extra_context:
20
+ context.update(extra_context)
21
+
22
+ serialized_context = _serialize_context(context)
23
+
24
+ execution = TaskExecution(
25
+ task_id=task.task_id,
26
+ task_model=task._meta.label,
27
+ trigger_event=trigger_event,
28
+ executor_class=executor_class_path,
29
+ status='pending',
30
+ context=json.dumps(serialized_context, default=str) if serialized_context else None,
31
+ )
32
+ execution.save()
33
+
34
+ _publish_message(execution, routing_key=queue_name)
35
+
36
+ return execution
37
+
38
+
39
+ def _publish_message(execution, routing_key=None):
40
+ message = {
41
+ 'execution_id': str(execution.execution_id),
42
+ 'task_id': str(execution.task_id) if execution.task_id else None,
43
+ 'trigger_event': execution.trigger_event,
44
+ 'executor_class': execution.executor_class,
45
+ }
46
+
47
+ try:
48
+ from django_simpletask5.core.message_queue import get_message_queue
49
+ mq = get_message_queue()
50
+ mq.publish(
51
+ routing_key=routing_key or DJANGO_SIMPLETASK_DEFAULT_QUEUE,
52
+ message=message,
53
+ )
54
+ logger.debug('Published message for execution %s', execution.execution_id)
55
+ except Exception:
56
+ logger.exception('Failed to publish message for execution %s', execution.execution_id)
57
+
58
+
59
+ def _serialize_context(context):
60
+ if context is None:
61
+ return None
62
+ serialized = {}
63
+ for key, value in context.items():
64
+ if isinstance(value, dict):
65
+ serialized[key] = {k: _serialize_value(v) for k, v in value.items()}
66
+ else:
67
+ serialized[key] = _serialize_value(value)
68
+ return serialized
69
+
70
+
71
+ def _serialize_value(value):
72
+ if value is None or isinstance(value, (str, bool)):
73
+ return value
74
+ return str(value)
75
+
76
+
77
+ def _model_to_dict(instance):
78
+ data = {}
79
+ for field in instance._meta.fields:
80
+ value = getattr(instance, field.attname)
81
+ if value is None:
82
+ data[field.attname] = None
83
+ elif isinstance(value, bool):
84
+ data[field.attname] = value
85
+ elif isinstance(value, (int, float)):
86
+ data[field.attname] = str(value)
87
+ elif isinstance(value, str):
88
+ data[field.attname] = value
89
+ elif isinstance(value, bytes):
90
+ data[field.attname] = value.decode('utf-8', errors='replace')
91
+ else:
92
+ data[field.attname] = str(value)
93
+ return data