django-simpletask5 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/PKG-INFO +105 -9
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/README.md +102 -8
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/__init__.py +2 -0
- django_simpletask5-0.1.4/django_simpletask5/admin.py +388 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/apps.py +2 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/core/cronjob_registry.py +32 -12
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/core/defaults.py +2 -3
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/core/publisher.py +14 -1
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/dashboards.py +17 -25
- django_simpletask5-0.1.4/django_simpletask5/executors/archive.py +38 -0
- django_simpletask5-0.1.4/django_simpletask5/executors/base.py +20 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/executors/bash_script.py +1 -1
- django_simpletask5-0.1.4/django_simpletask5/executors/loader.py +17 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/executors/ping_pong.py +1 -1
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/executors/python_script.py +1 -1
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/executors/retry_timeout.py +1 -1
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/executors/simple_request.py +1 -1
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/executors/status_check.py +6 -15
- django_simpletask5-0.1.4/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- django_simpletask5-0.1.4/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.po +418 -0
- django_simpletask5-0.1.4/django_simpletask5/management/commands/django_simpletask_archive.py +93 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/management/commands/django_simpletask_crontab.py +16 -6
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/management/commands/django_simpletask_executor.py +21 -6
- django_simpletask5-0.1.4/django_simpletask5/migrations/0002_taskexecution_done_tasks_count_and_more.py +39 -0
- django_simpletask5-0.1.4/django_simpletask5/migrations/0003_alter_cronjob_options_remove_cronjob_name_and_more.py +46 -0
- django_simpletask5-0.1.4/django_simpletask5/migrations/0004_alter_cronjob_is_modified_by_user.py +25 -0
- django_simpletask5-0.1.4/django_simpletask5/migrations/0005_alter_cronjob_is_modified_by_user_help.py +22 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/models.py +27 -5
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/services/archive.py +82 -53
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5.egg-info/PKG-INFO +105 -9
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5.egg-info/SOURCES.txt +7 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5.egg-info/requires.txt +2 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/pyproject.toml +6 -1
- django_simpletask5-0.1.2/django_simpletask5/admin.py +0 -205
- django_simpletask5-0.1.2/django_simpletask5/executors/archive.py +0 -24
- django_simpletask5-0.1.2/django_simpletask5/executors/base.py +0 -10
- django_simpletask5-0.1.2/django_simpletask5/executors/loader.py +0 -17
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/LICENSE +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/core/__init__.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/core/executor_scanner.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/core/lock.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/core/message_queue.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/core/signals.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/core/worker_registry.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/executors/__init__.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/management/__init__.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/management/commands/__init__.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/management/commands/django_simpletask_sync_cronjobs.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/migrations/0001_initial.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/migrations/__init__.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5/services/__init__.py +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5.egg-info/dependency_links.txt +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/django_simpletask5.egg-info/top_level.txt +0 -0
- {django_simpletask5-0.1.2 → django_simpletask5-0.1.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-simpletask5
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Lightweight async task execution framework for Django
|
|
5
5
|
Author-email: rRR0VrFP <rrr0vrfp@qq.com>
|
|
6
6
|
Maintainer-email: rRR0VrFP <rrr0vrfp@qq.com>
|
|
@@ -27,6 +27,8 @@ Requires-Dist: globallock>=0.1.4
|
|
|
27
27
|
Requires-Dist: django-safe-fields>=0.2.3
|
|
28
28
|
Requires-Dist: django-app-requires>=0.3.6
|
|
29
29
|
Requires-Dist: django-admin-dashboards>=0.1.1
|
|
30
|
+
Requires-Dist: django-checkbox-normalize>=0.2.2
|
|
31
|
+
Requires-Dist: django-tabbed-changeform-admin>=0.1.9
|
|
30
32
|
Requires-Dist: cryptography>=48.0.0
|
|
31
33
|
Requires-Dist: croniter>=6.2.2
|
|
32
34
|
Requires-Dist: requests>=2.34.2
|
|
@@ -104,6 +106,87 @@ DJANGO_SIMPLETASK_LOCK_CONFIG = {
|
|
|
104
106
|
python manage.py migrate django_simpletask5
|
|
105
107
|
```
|
|
106
108
|
|
|
109
|
+
## 配置说明
|
|
110
|
+
|
|
111
|
+
所有配置项均为可选,设有合理的默认值。以下按功能分类说明。
|
|
112
|
+
|
|
113
|
+
### 消息队列(MQ)
|
|
114
|
+
|
|
115
|
+
默认使用内存队列(`memory://`),适合开发调试。生产环境建议切换为 RabbitMQ 或 Redis。
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# 方式一:直接指定完整 Broker URL(优先级最高)
|
|
119
|
+
DJANGO_SIMPLETASK_BROKER_URL = 'amqp://guest:guest@127.0.0.1:5672//'
|
|
120
|
+
|
|
121
|
+
# 方式二:分别指定类型和连接参数
|
|
122
|
+
DJANGO_SIMPLETASK_MESSAGE_QUEUE_TYPE = 'rabbitmq' # 'memory' | 'rabbitmq' | 'redis'
|
|
123
|
+
DJANGO_SIMPLETASK_RABBITMQ_HOST = '127.0.0.1'
|
|
124
|
+
DJANGO_SIMPLETASK_RABBITMQ_PORT = 5672
|
|
125
|
+
DJANGO_SIMPLETASK_RABBITMQ_USER = 'guest'
|
|
126
|
+
DJANGO_SIMPLETASK_RABBITMQ_PASSWORD = 'guest'
|
|
127
|
+
|
|
128
|
+
# Redis 作为 MQ 时:
|
|
129
|
+
# DJANGO_SIMPLETASK_MESSAGE_QUEUE_TYPE = 'redis'
|
|
130
|
+
# DJANGO_SIMPLETASK_REDIS_MQ_HOST = '127.0.0.1'
|
|
131
|
+
# DJANGO_SIMPLETASK_REDIS_MQ_PORT = 6379
|
|
132
|
+
# DJANGO_SIMPLETASK_REDIS_MQ_DB = 0
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 分布式锁
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
DJANGO_SIMPLETASK_LOCK_CONFIG = {
|
|
139
|
+
'global_lock_engine_class': 'globallock.redis_global_lock.RedisGlobalLock',
|
|
140
|
+
'global_lock_engine_options': {
|
|
141
|
+
'host': '127.0.0.1',
|
|
142
|
+
'port': 6379,
|
|
143
|
+
'db': 0,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
DJANGO_SIMPLETASK_LOCK_TIMEOUT = 600 # 锁超时时间(秒)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 队列路由
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
DJANGO_SIMPLETASK_DEFAULT_QUEUE = 'django_simpletask5.queue.default' # 默认队列
|
|
153
|
+
DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE = 'django_simpletask5.queue.high_priority' # 高优先级队列
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 执行与重试
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES = 3 # 默认最大重试次数
|
|
160
|
+
DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS = 600 # 默认执行超时时间(秒)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 归档
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
DJANGO_SIMPLETASK_ARCHIVE_PATH = 'django_simpletask5_archives' # 归档文件存储子目录
|
|
167
|
+
DJANGO_SIMPLETASK_ARCHIVE_SALT = 'django-simpletask5-archive' # 归档加密盐值(结合 SECRET_KEY 派生 AES 密钥)
|
|
168
|
+
DJANGO_SIMPLETASK_ARCHIVE_RETENTION_DAYS = 7 # 归档保留天数
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Cron
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC = True # 是否自动将代码注册的 Cron 同步到数据库
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 安全
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
# 脚本执行白名单(空列表表示不限制)
|
|
181
|
+
DJANGO_SIMPLETASK_SCRIPT_WHITELIST = ['/path/to/allowed/scripts']
|
|
182
|
+
|
|
183
|
+
# 自定义字段加密(默认使用 django-safe-fields 的默认加密)
|
|
184
|
+
DJANGO_SIMPLETASK_FIELD_CIPHER_CLASS = None # 自定义加密器类
|
|
185
|
+
DJANGO_SIMPLETASK_RESULT_PASSWORD = None # 执行结果加密密码
|
|
186
|
+
DJANGO_SIMPLETASK_ERROR_MSG_PASSWORD = None # 错误信息加密密码
|
|
187
|
+
DJANGO_SIMPLETASK_CONTEXT_PASSWORD = None # 上下文数据加密密码
|
|
188
|
+
```
|
|
189
|
+
|
|
107
190
|
## 快速开始
|
|
108
191
|
|
|
109
192
|
### 1. 定义任务模型
|
|
@@ -232,6 +315,27 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
|
|
|
232
315
|
|
|
233
316
|
## Releases
|
|
234
317
|
|
|
318
|
+
### 0.1.4
|
|
319
|
+
|
|
320
|
+
- **修复打包** — `pyproject.toml` 添加 `package-data` 配置,打包时包含 locale po/mo 文件,修复 i18n 翻译不生效的问题
|
|
321
|
+
- **补充依赖** — `requirements.txt` 和 `pyproject.toml` 添加缺失的 `django-checkbox-normalize`、`django-tabbed-changeform-admin` 依赖
|
|
322
|
+
- **完善文档** — README 新增完整配置说明章节,涵盖消息队列(MQ)、分布式锁、队列路由、归档、Cron、安全等全部配置项
|
|
323
|
+
|
|
324
|
+
### 0.1.3
|
|
325
|
+
|
|
326
|
+
- **Admin 界面大升级** — 集成 Tabbed ChangeForm 分页签展示字段,CronJob 后台支持直接执行/启用/禁用操作;TaskExecution 列表优化,execution_id 显示前8字符并支持点击复制,重试次数合并显示,全局字段中文翻译
|
|
327
|
+
- **归档管理增强** — 新增 `django_simpletask_archive` 管理命令,支持手动触发归档与过期清理;PurgeExpiredExecutor 定期自动清理过期记录;保留天数可配置;采用流式 AES-GCM 加密,大文件归档更高效
|
|
328
|
+
- **Dashboard 修复** — 卡片链接使用 `reverse` 正确跳转,无执行数据时成功率显示 `-`
|
|
329
|
+
- **超时检测优化** — 模型新增 `expire_time` 字段,超时判定更准确高效
|
|
330
|
+
|
|
331
|
+
### 0.1.2
|
|
332
|
+
|
|
333
|
+
- **修复**: 修复 Transaction-Message 排序竞态条件,使用 `transaction.on_commit` 确保消息在事务提交后才发送到队列
|
|
334
|
+
|
|
335
|
+
### 0.1.1
|
|
336
|
+
|
|
337
|
+
- **修复**: 修复 `DjangoSimpletask5Config` 缺少 `ready()` 方法导致信号监听未注册的问题
|
|
338
|
+
|
|
235
339
|
### 0.1.0
|
|
236
340
|
|
|
237
341
|
这是 django-simpletask5 的首个正式版本。核心功能包括:
|
|
@@ -246,14 +350,6 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
|
|
|
246
350
|
- **Worker 注册中心** — 基于 Redis 的 Worker 心跳与状态追踪
|
|
247
351
|
- **Django Admin 集成** — 完整的后台管理界面与仪表盘
|
|
248
352
|
|
|
249
|
-
### 0.1.2
|
|
250
|
-
|
|
251
|
-
- **修复**: 修复 Transaction-Message 排序竞态条件,使用 `transaction.on_commit` 确保消息在事务提交后才发送到队列
|
|
252
|
-
|
|
253
|
-
### 0.1.1
|
|
254
|
-
|
|
255
|
-
- **修复**: 修复 `DjangoSimpletask5Config` 缺少 `ready()` 方法导致信号监听未注册的问题
|
|
256
|
-
|
|
257
353
|
## 许可证
|
|
258
354
|
|
|
259
355
|
MIT
|
|
@@ -67,6 +67,87 @@ DJANGO_SIMPLETASK_LOCK_CONFIG = {
|
|
|
67
67
|
python manage.py migrate django_simpletask5
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
## 配置说明
|
|
71
|
+
|
|
72
|
+
所有配置项均为可选,设有合理的默认值。以下按功能分类说明。
|
|
73
|
+
|
|
74
|
+
### 消息队列(MQ)
|
|
75
|
+
|
|
76
|
+
默认使用内存队列(`memory://`),适合开发调试。生产环境建议切换为 RabbitMQ 或 Redis。
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# 方式一:直接指定完整 Broker URL(优先级最高)
|
|
80
|
+
DJANGO_SIMPLETASK_BROKER_URL = 'amqp://guest:guest@127.0.0.1:5672//'
|
|
81
|
+
|
|
82
|
+
# 方式二:分别指定类型和连接参数
|
|
83
|
+
DJANGO_SIMPLETASK_MESSAGE_QUEUE_TYPE = 'rabbitmq' # 'memory' | 'rabbitmq' | 'redis'
|
|
84
|
+
DJANGO_SIMPLETASK_RABBITMQ_HOST = '127.0.0.1'
|
|
85
|
+
DJANGO_SIMPLETASK_RABBITMQ_PORT = 5672
|
|
86
|
+
DJANGO_SIMPLETASK_RABBITMQ_USER = 'guest'
|
|
87
|
+
DJANGO_SIMPLETASK_RABBITMQ_PASSWORD = 'guest'
|
|
88
|
+
|
|
89
|
+
# Redis 作为 MQ 时:
|
|
90
|
+
# DJANGO_SIMPLETASK_MESSAGE_QUEUE_TYPE = 'redis'
|
|
91
|
+
# DJANGO_SIMPLETASK_REDIS_MQ_HOST = '127.0.0.1'
|
|
92
|
+
# DJANGO_SIMPLETASK_REDIS_MQ_PORT = 6379
|
|
93
|
+
# DJANGO_SIMPLETASK_REDIS_MQ_DB = 0
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 分布式锁
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
DJANGO_SIMPLETASK_LOCK_CONFIG = {
|
|
100
|
+
'global_lock_engine_class': 'globallock.redis_global_lock.RedisGlobalLock',
|
|
101
|
+
'global_lock_engine_options': {
|
|
102
|
+
'host': '127.0.0.1',
|
|
103
|
+
'port': 6379,
|
|
104
|
+
'db': 0,
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
DJANGO_SIMPLETASK_LOCK_TIMEOUT = 600 # 锁超时时间(秒)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 队列路由
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
DJANGO_SIMPLETASK_DEFAULT_QUEUE = 'django_simpletask5.queue.default' # 默认队列
|
|
114
|
+
DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE = 'django_simpletask5.queue.high_priority' # 高优先级队列
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 执行与重试
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES = 3 # 默认最大重试次数
|
|
121
|
+
DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS = 600 # 默认执行超时时间(秒)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 归档
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
DJANGO_SIMPLETASK_ARCHIVE_PATH = 'django_simpletask5_archives' # 归档文件存储子目录
|
|
128
|
+
DJANGO_SIMPLETASK_ARCHIVE_SALT = 'django-simpletask5-archive' # 归档加密盐值(结合 SECRET_KEY 派生 AES 密钥)
|
|
129
|
+
DJANGO_SIMPLETASK_ARCHIVE_RETENTION_DAYS = 7 # 归档保留天数
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Cron
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC = True # 是否自动将代码注册的 Cron 同步到数据库
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 安全
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# 脚本执行白名单(空列表表示不限制)
|
|
142
|
+
DJANGO_SIMPLETASK_SCRIPT_WHITELIST = ['/path/to/allowed/scripts']
|
|
143
|
+
|
|
144
|
+
# 自定义字段加密(默认使用 django-safe-fields 的默认加密)
|
|
145
|
+
DJANGO_SIMPLETASK_FIELD_CIPHER_CLASS = None # 自定义加密器类
|
|
146
|
+
DJANGO_SIMPLETASK_RESULT_PASSWORD = None # 执行结果加密密码
|
|
147
|
+
DJANGO_SIMPLETASK_ERROR_MSG_PASSWORD = None # 错误信息加密密码
|
|
148
|
+
DJANGO_SIMPLETASK_CONTEXT_PASSWORD = None # 上下文数据加密密码
|
|
149
|
+
```
|
|
150
|
+
|
|
70
151
|
## 快速开始
|
|
71
152
|
|
|
72
153
|
### 1. 定义任务模型
|
|
@@ -195,6 +276,27 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
|
|
|
195
276
|
|
|
196
277
|
## Releases
|
|
197
278
|
|
|
279
|
+
### 0.1.4
|
|
280
|
+
|
|
281
|
+
- **修复打包** — `pyproject.toml` 添加 `package-data` 配置,打包时包含 locale po/mo 文件,修复 i18n 翻译不生效的问题
|
|
282
|
+
- **补充依赖** — `requirements.txt` 和 `pyproject.toml` 添加缺失的 `django-checkbox-normalize`、`django-tabbed-changeform-admin` 依赖
|
|
283
|
+
- **完善文档** — README 新增完整配置说明章节,涵盖消息队列(MQ)、分布式锁、队列路由、归档、Cron、安全等全部配置项
|
|
284
|
+
|
|
285
|
+
### 0.1.3
|
|
286
|
+
|
|
287
|
+
- **Admin 界面大升级** — 集成 Tabbed ChangeForm 分页签展示字段,CronJob 后台支持直接执行/启用/禁用操作;TaskExecution 列表优化,execution_id 显示前8字符并支持点击复制,重试次数合并显示,全局字段中文翻译
|
|
288
|
+
- **归档管理增强** — 新增 `django_simpletask_archive` 管理命令,支持手动触发归档与过期清理;PurgeExpiredExecutor 定期自动清理过期记录;保留天数可配置;采用流式 AES-GCM 加密,大文件归档更高效
|
|
289
|
+
- **Dashboard 修复** — 卡片链接使用 `reverse` 正确跳转,无执行数据时成功率显示 `-`
|
|
290
|
+
- **超时检测优化** — 模型新增 `expire_time` 字段,超时判定更准确高效
|
|
291
|
+
|
|
292
|
+
### 0.1.2
|
|
293
|
+
|
|
294
|
+
- **修复**: 修复 Transaction-Message 排序竞态条件,使用 `transaction.on_commit` 确保消息在事务提交后才发送到队列
|
|
295
|
+
|
|
296
|
+
### 0.1.1
|
|
297
|
+
|
|
298
|
+
- **修复**: 修复 `DjangoSimpletask5Config` 缺少 `ready()` 方法导致信号监听未注册的问题
|
|
299
|
+
|
|
198
300
|
### 0.1.0
|
|
199
301
|
|
|
200
302
|
这是 django-simpletask5 的首个正式版本。核心功能包括:
|
|
@@ -209,14 +311,6 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
|
|
|
209
311
|
- **Worker 注册中心** — 基于 Redis 的 Worker 心跳与状态追踪
|
|
210
312
|
- **Django Admin 集成** — 完整的后台管理界面与仪表盘
|
|
211
313
|
|
|
212
|
-
### 0.1.2
|
|
213
|
-
|
|
214
|
-
- **修复**: 修复 Transaction-Message 排序竞态条件,使用 `transaction.on_commit` 确保消息在事务提交后才发送到队列
|
|
215
|
-
|
|
216
|
-
### 0.1.1
|
|
217
|
-
|
|
218
|
-
- **修复**: 修复 `DjangoSimpletask5Config` 缺少 `ready()` 方法导致信号监听未注册的问题
|
|
219
|
-
|
|
220
314
|
## 许可证
|
|
221
315
|
|
|
222
316
|
MIT
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from django import forms
|
|
4
|
+
from django.contrib import admin
|
|
5
|
+
from django.http import HttpResponseRedirect
|
|
6
|
+
from django.urls import reverse
|
|
7
|
+
from django.utils.translation import gettext_lazy as _
|
|
8
|
+
from django_checkbox_normalize.admin import DjangoCheckboxNormalizeAdmin
|
|
9
|
+
from django_tabbed_changeform_admin.admin import DjangoTabbedChangeformAdmin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from django_simpletask5.models import TaskExecution, CronJob, TaskExecutionStat, TaskExecutionArchive
|
|
13
|
+
from django_simpletask5.core.executor_scanner import (
|
|
14
|
+
get_executor_choices,
|
|
15
|
+
get_executor_schema_map,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExecutorClassWidget(forms.Select):
|
|
20
|
+
def __init__(self, attrs=None):
|
|
21
|
+
schema_map = get_executor_schema_map()
|
|
22
|
+
choices = [("", "---------")]
|
|
23
|
+
for group_name, items in get_executor_choices():
|
|
24
|
+
group_choices = [(v, v) for v, _ in items]
|
|
25
|
+
choices.append((group_name, group_choices))
|
|
26
|
+
attrs = attrs or {}
|
|
27
|
+
attrs.setdefault("class", "select2-executor")
|
|
28
|
+
attrs.setdefault("style", "width: 600px;")
|
|
29
|
+
attrs["data-schemas"] = json.dumps(schema_map, ensure_ascii=False)
|
|
30
|
+
super().__init__(attrs=attrs, choices=choices)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CronJobForm(forms.ModelForm):
|
|
34
|
+
uid = forms.CharField(
|
|
35
|
+
label=_("UID"),
|
|
36
|
+
required=True,
|
|
37
|
+
widget=forms.TextInput(attrs={'style': 'width: 600px;'}),
|
|
38
|
+
)
|
|
39
|
+
display_name = forms.CharField(
|
|
40
|
+
label=_("Name"),
|
|
41
|
+
required=False,
|
|
42
|
+
widget=forms.TextInput(attrs={'style': 'width: 600px;'}),
|
|
43
|
+
)
|
|
44
|
+
executor_class = forms.CharField(
|
|
45
|
+
label=_("Executor class"),
|
|
46
|
+
widget=ExecutorClassWidget(),
|
|
47
|
+
)
|
|
48
|
+
context = forms.CharField(
|
|
49
|
+
label=_("Context"),
|
|
50
|
+
required=False,
|
|
51
|
+
widget=forms.Textarea(
|
|
52
|
+
attrs={
|
|
53
|
+
"rows": 8,
|
|
54
|
+
"style": "width: 600px; font-family: monospace;",
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
help_text=_("Supports JSON or YAML format. YAML is more concise and compatible with JSON standard format."),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def __init__(self, *args, **kwargs):
|
|
61
|
+
super().__init__(*args, **kwargs)
|
|
62
|
+
if self.instance and self.instance.pk:
|
|
63
|
+
self.fields['uid'].disabled = True
|
|
64
|
+
if self.instance and self.instance.pk and self.instance.context:
|
|
65
|
+
try:
|
|
66
|
+
parsed = json.loads(self.instance.context)
|
|
67
|
+
import yaml
|
|
68
|
+
|
|
69
|
+
self.initial["context"] = yaml.dump(
|
|
70
|
+
parsed,
|
|
71
|
+
default_flow_style=False,
|
|
72
|
+
allow_unicode=True,
|
|
73
|
+
sort_keys=False,
|
|
74
|
+
).strip()
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
def clean_context(self):
|
|
79
|
+
raw = self.cleaned_data.get("context")
|
|
80
|
+
if not raw:
|
|
81
|
+
return None
|
|
82
|
+
try:
|
|
83
|
+
parsed = json.loads(raw)
|
|
84
|
+
return json.dumps(parsed, ensure_ascii=False)
|
|
85
|
+
except json.JSONDecodeError:
|
|
86
|
+
pass
|
|
87
|
+
try:
|
|
88
|
+
import yaml
|
|
89
|
+
|
|
90
|
+
parsed = yaml.safe_load(raw)
|
|
91
|
+
if parsed is None:
|
|
92
|
+
return None
|
|
93
|
+
return json.dumps(parsed, ensure_ascii=False)
|
|
94
|
+
except Exception:
|
|
95
|
+
raise forms.ValidationError(_("Invalid parameter format. Please use JSON or YAML format."))
|
|
96
|
+
|
|
97
|
+
class Meta:
|
|
98
|
+
model = CronJob
|
|
99
|
+
fields = "__all__"
|
|
100
|
+
|
|
101
|
+
class Media:
|
|
102
|
+
css = {
|
|
103
|
+
"all": ["admin/css/vendor/select2/select2.min.css"],
|
|
104
|
+
}
|
|
105
|
+
js = [
|
|
106
|
+
"admin/js/vendor/jquery/jquery.min.js",
|
|
107
|
+
"admin/js/vendor/select2/select2.full.min.js",
|
|
108
|
+
"django_simpletask5/js/select2_executor.js",
|
|
109
|
+
"admin/js/jquery.init.js",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@admin.register(TaskExecution)
|
|
114
|
+
class TaskExecutionAdmin(DjangoCheckboxNormalizeAdmin, DjangoTabbedChangeformAdmin):
|
|
115
|
+
list_display = [
|
|
116
|
+
"executor_class",
|
|
117
|
+
"short_execution_id",
|
|
118
|
+
"trigger_event",
|
|
119
|
+
"status",
|
|
120
|
+
"retry_info",
|
|
121
|
+
"created_at",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
list_display_links = ["executor_class"]
|
|
125
|
+
|
|
126
|
+
@admin.display(description=_("Execution ID"))
|
|
127
|
+
def short_execution_id(self, obj):
|
|
128
|
+
from django.utils.html import format_html
|
|
129
|
+
full = str(obj.execution_id)
|
|
130
|
+
short = full[:8]
|
|
131
|
+
copied_text = _("Copied!")
|
|
132
|
+
return format_html(
|
|
133
|
+
'<span style="cursor:pointer;border-bottom:1px dashed #999;"'
|
|
134
|
+
' title="{}" onclick="navigator.clipboard.writeText(\'{}\').then(function(){{'
|
|
135
|
+
'var t=this;var o=t.textContent;t.textContent=\'{}\';'
|
|
136
|
+
'setTimeout(function(){{t.textContent=o;}},800);}}.bind(this)).catch(function(){{}})">{}</span>',
|
|
137
|
+
full, full, copied_text, short
|
|
138
|
+
)
|
|
139
|
+
list_filter = ["status", "trigger_event", "created_at"]
|
|
140
|
+
search_fields = ["execution_id", "task_id", "executor_class"]
|
|
141
|
+
readonly_fields = [
|
|
142
|
+
"execution_id", "task_id", "task_model", "trigger_event", "executor_class", "status",
|
|
143
|
+
"result", "error_message", "context", "total_tasks_count", "done_tasks_count",
|
|
144
|
+
"retry_count", "max_retries", "timeout_seconds", "expire_time",
|
|
145
|
+
"started_at", "finished_at", "created_at", "updated_at",
|
|
146
|
+
]
|
|
147
|
+
ordering = ["-created_at"]
|
|
148
|
+
|
|
149
|
+
def has_add_permission(self, request):
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def has_delete_permission(self, request, obj=None):
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
|
|
156
|
+
extra_context = extra_context or {}
|
|
157
|
+
extra_context["show_save"] = False
|
|
158
|
+
extra_context["show_save_and_continue"] = False
|
|
159
|
+
extra_context["show_save_and_add_another"] = False
|
|
160
|
+
extra_context["show_delete_link"] = False
|
|
161
|
+
return super().changeform_view(request, object_id, form_url, extra_context)
|
|
162
|
+
|
|
163
|
+
fieldsets = [
|
|
164
|
+
(None, {
|
|
165
|
+
"fields": ["execution_id", "task_id", "task_model", "trigger_event", "executor_class", "status", "created_at", "updated_at"],
|
|
166
|
+
"classes": ["tab-basic"],
|
|
167
|
+
}),
|
|
168
|
+
(None, {
|
|
169
|
+
"fields": ["result", "error_message", "context", "total_tasks_count", "done_tasks_count", "started_at", "finished_at"],
|
|
170
|
+
"classes": ["tab-data"],
|
|
171
|
+
}),
|
|
172
|
+
(None, {
|
|
173
|
+
"fields": ["retry_count", "max_retries", "timeout_seconds", "expire_time"],
|
|
174
|
+
"classes": ["tab-retry"],
|
|
175
|
+
}),
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
tabs = [
|
|
179
|
+
(_("Basic Info"), ["tab-basic"]),
|
|
180
|
+
(_("Execution Data"), ["tab-data"]),
|
|
181
|
+
(_("Retry & Timeout"), ["tab-retry"]),
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
@admin.display(description=_("Retry"))
|
|
185
|
+
def retry_info(self, obj):
|
|
186
|
+
return f"{obj.retry_count}/{obj.max_retries}"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@admin.register(CronJob)
|
|
190
|
+
class CronJobAdmin(DjangoCheckboxNormalizeAdmin, DjangoTabbedChangeformAdmin):
|
|
191
|
+
form = CronJobForm
|
|
192
|
+
list_display = [
|
|
193
|
+
"display_name",
|
|
194
|
+
"cron_expression",
|
|
195
|
+
"executor_class",
|
|
196
|
+
"is_active",
|
|
197
|
+
"is_modified_by_user",
|
|
198
|
+
"updated_at",
|
|
199
|
+
"action_buttons",
|
|
200
|
+
]
|
|
201
|
+
readonly_fields = ["created_at", "updated_at"]
|
|
202
|
+
list_filter = ["is_active", "is_modified_by_user"]
|
|
203
|
+
search_fields = ["display_name", "uid", "executor_class"]
|
|
204
|
+
|
|
205
|
+
fieldsets = [
|
|
206
|
+
(None, {
|
|
207
|
+
"fields": ["uid", "display_name", "executor_class", "cron_expression", "description", "created_at", "updated_at"],
|
|
208
|
+
"classes": ["tab-basic"],
|
|
209
|
+
}),
|
|
210
|
+
(None, {
|
|
211
|
+
"fields": ["is_active", "context", "is_modified_by_user", "next_run_time"],
|
|
212
|
+
"classes": ["tab-advanced"],
|
|
213
|
+
}),
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
tabs = [
|
|
217
|
+
(_("Basic Config"), ["tab-basic"]),
|
|
218
|
+
(_("Advanced Settings"), ["tab-advanced"]),
|
|
219
|
+
]
|
|
220
|
+
actions = [
|
|
221
|
+
"reset_from_code",
|
|
222
|
+
"mark_as_modified",
|
|
223
|
+
"unmark_as_modified",
|
|
224
|
+
"activate_cronjobs",
|
|
225
|
+
"deactivate_cronjobs",
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
def get_queryset(self, request):
|
|
229
|
+
return super().get_queryset(request)
|
|
230
|
+
|
|
231
|
+
def action_buttons(self, obj):
|
|
232
|
+
from django.utils.html import format_html
|
|
233
|
+
from django.urls import reverse
|
|
234
|
+
|
|
235
|
+
changelist_url = reverse('admin:django_simpletask5_cronjob_changelist')
|
|
236
|
+
|
|
237
|
+
buttons = []
|
|
238
|
+
buttons.append(
|
|
239
|
+
f'<a class="button" style="background:#17a2b8;color:#fff;padding:3px 8px;border-radius:3px;text-decoration:none;margin-right:4px;font-size:12px" '
|
|
240
|
+
f'href="{changelist_url}?action=run&id={obj.pk}" title="立即执行">'
|
|
241
|
+
f'<i class="ri-play-line"></i> 执行</a>'
|
|
242
|
+
)
|
|
243
|
+
if obj.is_active:
|
|
244
|
+
buttons.append(
|
|
245
|
+
f'<a class="button" style="background:#ffc107;color:#000;padding:3px 8px;border-radius:3px;text-decoration:none;margin-right:4px;font-size:12px" '
|
|
246
|
+
f'href="{changelist_url}?action=toggle-active&id={obj.pk}" title="禁用">'
|
|
247
|
+
f'<i class="ri-pause-line"></i> 禁用</a>'
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
buttons.append(
|
|
251
|
+
f'<a class="button" style="background:#28a745;color:#fff;padding:3px 8px;border-radius:3px;text-decoration:none;margin-right:4px;font-size:12px" '
|
|
252
|
+
f'href="{changelist_url}?action=toggle-active&id={obj.pk}" title="启用">'
|
|
253
|
+
f'<i class="ri-play-circle-line"></i> 启用</a>'
|
|
254
|
+
)
|
|
255
|
+
delete_url = reverse('admin:django_simpletask5_cronjob_delete', args=[obj.pk])
|
|
256
|
+
buttons.append(
|
|
257
|
+
f'<a class="button" style="background:#dc3545;color:#fff;padding:3px 8px;border-radius:3px;text-decoration:none;margin-right:4px;font-size:12px" '
|
|
258
|
+
f'href="{delete_url}" title="删除">'
|
|
259
|
+
f'<i class="ri-delete-bin-line"></i> 删除</a>'
|
|
260
|
+
)
|
|
261
|
+
return format_html(''.join(buttons))
|
|
262
|
+
|
|
263
|
+
action_buttons.short_description = _("操作")
|
|
264
|
+
action_buttons.allow_tags = True
|
|
265
|
+
|
|
266
|
+
def changelist_view(self, request, extra_context=None):
|
|
267
|
+
action = request.GET.get('action')
|
|
268
|
+
object_id = request.GET.get('id')
|
|
269
|
+
if action and object_id:
|
|
270
|
+
from django_simpletask5.models import CronJob
|
|
271
|
+
try:
|
|
272
|
+
cronjob = CronJob.objects.get(pk=object_id)
|
|
273
|
+
if action == 'run':
|
|
274
|
+
from django_simpletask5.management.commands.django_simpletask_crontab import Command as CrontabCommand
|
|
275
|
+
cmd = CrontabCommand()
|
|
276
|
+
cmd._trigger_cronjob(cronjob)
|
|
277
|
+
self.message_user(request, f'Cronjob "{cronjob.display_name}" triggered.')
|
|
278
|
+
elif action == 'toggle-active':
|
|
279
|
+
cronjob.is_active = not cronjob.is_active
|
|
280
|
+
cronjob.save(update_fields=['is_active'])
|
|
281
|
+
status = 'activated' if cronjob.is_active else 'deactivated'
|
|
282
|
+
self.message_user(request, f'Cronjob "{cronjob.display_name}" {status}.')
|
|
283
|
+
except CronJob.DoesNotExist:
|
|
284
|
+
self.message_user(request, f'Cronjob not found.', level='ERROR')
|
|
285
|
+
return HttpResponseRedirect(request.path)
|
|
286
|
+
return super().changelist_view(request, extra_context=extra_context)
|
|
287
|
+
|
|
288
|
+
def save_model(self, request, obj, form, change):
|
|
289
|
+
if change:
|
|
290
|
+
SUBSTANTIVE_FIELDS = {"cron_expression", "executor_class", "context", "description", "display_name", "is_active"}
|
|
291
|
+
changed = set(form.changed_data)
|
|
292
|
+
if changed & SUBSTANTIVE_FIELDS:
|
|
293
|
+
obj.is_modified_by_user = True
|
|
294
|
+
super().save_model(request, obj, form, change)
|
|
295
|
+
|
|
296
|
+
def save_related(self, request, form, formsets, change):
|
|
297
|
+
super().save_related(request, form, formsets, change)
|
|
298
|
+
|
|
299
|
+
def reset_from_code(self, request, queryset):
|
|
300
|
+
from django_simpletask5.core.cronjob_registry import _CRONJOB_REGISTRY
|
|
301
|
+
|
|
302
|
+
updated = 0
|
|
303
|
+
for cronjob in queryset:
|
|
304
|
+
if cronjob.uid in _CRONJOB_REGISTRY:
|
|
305
|
+
registered = _CRONJOB_REGISTRY[cronjob.uid]
|
|
306
|
+
cronjob.display_name = registered.get("display_name", "")
|
|
307
|
+
cronjob.cron_expression = registered["cron_expression"]
|
|
308
|
+
cronjob.executor_class = registered["executor_class"]
|
|
309
|
+
cronjob.context = registered.get("context")
|
|
310
|
+
cronjob.description = registered.get("description", "")
|
|
311
|
+
cronjob.is_modified_by_user = False
|
|
312
|
+
cronjob.save()
|
|
313
|
+
updated += 1
|
|
314
|
+
self.message_user(request, f"{updated} cronjob(s) reset from code definitions.")
|
|
315
|
+
|
|
316
|
+
reset_from_code.short_description = _("Reset from code definitions")
|
|
317
|
+
|
|
318
|
+
def mark_as_modified(self, request, queryset):
|
|
319
|
+
updated = queryset.update(is_modified_by_user=True)
|
|
320
|
+
self.message_user(request, f"{updated} cronjob(s) marked as user-modified.")
|
|
321
|
+
|
|
322
|
+
mark_as_modified.short_description = _("Mark as user-modified")
|
|
323
|
+
|
|
324
|
+
def unmark_as_modified(self, request, queryset):
|
|
325
|
+
updated = queryset.update(is_modified_by_user=False)
|
|
326
|
+
self.message_user(request, f"{updated} cronjob(s) unmarked as user-modified.")
|
|
327
|
+
|
|
328
|
+
unmark_as_modified.short_description = _("Unmark as user-modified")
|
|
329
|
+
|
|
330
|
+
def activate_cronjobs(self, request, queryset):
|
|
331
|
+
updated = queryset.update(is_active=True)
|
|
332
|
+
self.message_user(request, f"{updated} cronjob(s) activated.")
|
|
333
|
+
|
|
334
|
+
activate_cronjobs.short_description = _("Activate selected tasks")
|
|
335
|
+
|
|
336
|
+
def deactivate_cronjobs(self, request, queryset):
|
|
337
|
+
updated = queryset.update(is_active=False)
|
|
338
|
+
self.message_user(request, f"{updated} cronjob(s) deactivated.")
|
|
339
|
+
|
|
340
|
+
deactivate_cronjobs.short_description = _("Deactivate selected tasks")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@admin.register(TaskExecutionStat)
|
|
344
|
+
class TaskExecutionStatAdmin(DjangoCheckboxNormalizeAdmin, DjangoTabbedChangeformAdmin):
|
|
345
|
+
list_display = [
|
|
346
|
+
"date",
|
|
347
|
+
"trigger_event",
|
|
348
|
+
"total_count",
|
|
349
|
+
"success_count",
|
|
350
|
+
"failed_count",
|
|
351
|
+
"avg_duration_seconds",
|
|
352
|
+
]
|
|
353
|
+
list_filter = ["date", "trigger_event"]
|
|
354
|
+
ordering = ["-date"]
|
|
355
|
+
|
|
356
|
+
fieldsets = [
|
|
357
|
+
(None, {
|
|
358
|
+
"fields": ["date", "task_model", "trigger_event", "created_at"],
|
|
359
|
+
"classes": ["tab-basic"],
|
|
360
|
+
}),
|
|
361
|
+
(None, {
|
|
362
|
+
"fields": ["total_count", "success_count", "failed_count", "timeout_count", "avg_duration_seconds"],
|
|
363
|
+
"classes": ["tab-stats"],
|
|
364
|
+
}),
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
tabs = [
|
|
368
|
+
(_("Basic Info"), ["tab-basic"]),
|
|
369
|
+
(_("Statistics"), ["tab-stats"]),
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@admin.register(TaskExecutionArchive)
|
|
374
|
+
class TaskExecutionArchiveAdmin(DjangoCheckboxNormalizeAdmin, DjangoTabbedChangeformAdmin):
|
|
375
|
+
list_display = ["archive_date", "created_at"]
|
|
376
|
+
readonly_fields = ["archive_date", "file", "created_at"]
|
|
377
|
+
ordering = ["-archive_date"]
|
|
378
|
+
|
|
379
|
+
fieldsets = [
|
|
380
|
+
(None, {
|
|
381
|
+
"fields": ["archive_date", "file", "created_at"],
|
|
382
|
+
"classes": ["tab-basic"],
|
|
383
|
+
}),
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
tabs = [
|
|
387
|
+
(_("Basic Info"), ["tab-basic"]),
|
|
388
|
+
]
|