django-celeryx 0.1.0a1__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_celeryx-0.1.0a1/.gitignore +35 -0
- django_celeryx-0.1.0a1/LICENSE +21 -0
- django_celeryx-0.1.0a1/PKG-INFO +148 -0
- django_celeryx-0.1.0a1/README.md +106 -0
- django_celeryx-0.1.0a1/django_celeryx/__init__.py +10 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/__init__.py +1 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/admin.py +331 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/apps.py +52 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/helpers.py +5 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/migrations/0001_initial.py +148 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/migrations/0002_remove_taskevent_celeryx_task_uuid_idx_and_more.py +34 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/migrations/0003_rename_event_to_state.py +20 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/migrations/__init__.py +0 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/models.py +163 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/queryset.py +815 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/change_list.html +62 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/dashboard/change_list.html +281 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/task/apply.html +70 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/task/change_form.html +186 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/task/change_list.html +9 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/worker/change_form.html +352 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/worker/change_list.html +34 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/templatetags/__init__.py +0 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/templatetags/celeryx_tags.py +59 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/views/__init__.py +1 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/views/apply_task.py +98 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/views/dashboard.py +191 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/views/task_detail.py +95 -0
- django_celeryx-0.1.0a1/django_celeryx/admin/views/worker_detail.py +217 -0
- django_celeryx-0.1.0a1/django_celeryx/apps.py +5 -0
- django_celeryx-0.1.0a1/django_celeryx/control/__init__.py +1 -0
- django_celeryx-0.1.0a1/django_celeryx/control/tasks.py +59 -0
- django_celeryx-0.1.0a1/django_celeryx/control/workers.py +63 -0
- django_celeryx-0.1.0a1/django_celeryx/db_models.py +78 -0
- django_celeryx-0.1.0a1/django_celeryx/db_router.py +49 -0
- django_celeryx-0.1.0a1/django_celeryx/helpers.py +25 -0
- django_celeryx-0.1.0a1/django_celeryx/metrics.py +172 -0
- django_celeryx-0.1.0a1/django_celeryx/py.typed +0 -0
- django_celeryx-0.1.0a1/django_celeryx/settings.py +149 -0
- django_celeryx-0.1.0a1/django_celeryx/state/__init__.py +1 -0
- django_celeryx-0.1.0a1/django_celeryx/state/events.py +301 -0
- django_celeryx-0.1.0a1/django_celeryx/state/persistence.py +146 -0
- django_celeryx-0.1.0a1/django_celeryx/types.py +43 -0
- django_celeryx-0.1.0a1/django_celeryx/unfold/__init__.py +1 -0
- django_celeryx-0.1.0a1/django_celeryx/unfold/admin.py +244 -0
- django_celeryx-0.1.0a1/django_celeryx/unfold/apps.py +29 -0
- django_celeryx-0.1.0a1/django_celeryx/unfold/models.py +5 -0
- django_celeryx-0.1.0a1/django_celeryx/urls.py +25 -0
- django_celeryx-0.1.0a1/pyproject.toml +220 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
*.py[cod]
|
|
2
|
+
__pycache__/
|
|
3
|
+
.DS_Store
|
|
4
|
+
*.sql
|
|
5
|
+
*.bz2
|
|
6
|
+
*~
|
|
7
|
+
*.log
|
|
8
|
+
*.json
|
|
9
|
+
*.wsgi
|
|
10
|
+
local_settings.py
|
|
11
|
+
development_settings.py
|
|
12
|
+
*.egg-info
|
|
13
|
+
.project
|
|
14
|
+
.pydevproject
|
|
15
|
+
.settings
|
|
16
|
+
versiontools*
|
|
17
|
+
_build*
|
|
18
|
+
doc/index.html
|
|
19
|
+
/build/
|
|
20
|
+
/dist/
|
|
21
|
+
*.swp
|
|
22
|
+
\#*
|
|
23
|
+
.\#*
|
|
24
|
+
.tox
|
|
25
|
+
dump.rdb
|
|
26
|
+
.idea
|
|
27
|
+
.venv
|
|
28
|
+
.coverage
|
|
29
|
+
coverage.xml
|
|
30
|
+
cobertura.xml
|
|
31
|
+
CLAUDE.md
|
|
32
|
+
site/
|
|
33
|
+
*.sqlite3
|
|
34
|
+
*.sqlite3-shm
|
|
35
|
+
*.sqlite3-wal
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oliver Haas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-celeryx
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Celery monitoring and management for Django admin
|
|
5
|
+
Project-URL: Homepage, https://github.com/oliverhaas/django-celeryx
|
|
6
|
+
Project-URL: Documentation, https://oliverhaas.github.io/django-celeryx/
|
|
7
|
+
Project-URL: Repository, https://github.com/oliverhaas/django-celeryx.git
|
|
8
|
+
Project-URL: Changelog, https://oliverhaas.github.io/django-celeryx/reference/changelog/
|
|
9
|
+
Author-email: Oliver Haas <ohaas@e1plus.de>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: admin,celery,django,flower,monitoring
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Classifier: Framework :: Django
|
|
16
|
+
Classifier: Framework :: Django :: 5.2
|
|
17
|
+
Classifier: Framework :: Django :: 6.0
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python
|
|
22
|
+
Classifier: Programming Language :: Python :: 3
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
27
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
28
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
29
|
+
Classifier: Topic :: System :: Monitoring
|
|
30
|
+
Classifier: Typing :: Typed
|
|
31
|
+
Requires-Python: >=3.12
|
|
32
|
+
Requires-Dist: django<7,>=5.2
|
|
33
|
+
Provides-Extra: celery
|
|
34
|
+
Requires-Dist: celery<6,>=5.4; extra == 'celery'
|
|
35
|
+
Provides-Extra: celery-asyncio
|
|
36
|
+
Requires-Dist: celery-asyncio>=6.0.0a1; extra == 'celery-asyncio'
|
|
37
|
+
Provides-Extra: prometheus
|
|
38
|
+
Requires-Dist: prometheus-client>=0.20.0; extra == 'prometheus'
|
|
39
|
+
Provides-Extra: unfold
|
|
40
|
+
Requires-Dist: django-unfold>=0.70.0; extra == 'unfold'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# django-celeryx
|
|
44
|
+
|
|
45
|
+
[](https://pypi.org/project/django-celeryx/)
|
|
46
|
+
[](https://pypi.org/project/django-celeryx/)
|
|
47
|
+
[](https://github.com/oliverhaas/django-celeryx/actions/workflows/ci.yml)
|
|
48
|
+
|
|
49
|
+
Celery monitoring and management for Django admin. Like Flower, but embedded in your Django admin with htmx for real-time updates.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```console
|
|
54
|
+
pip install django-celeryx[celery]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or with celery-asyncio:
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
pip install django-celeryx[celery-asyncio]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
INSTALLED_APPS = [
|
|
67
|
+
# ...
|
|
68
|
+
"django_celeryx.admin",
|
|
69
|
+
]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
That's it. Start your Django server and navigate to the admin to see your Celery tasks and workers.
|
|
73
|
+
|
|
74
|
+
## Features
|
|
75
|
+
|
|
76
|
+
- **Real-time task monitoring** — Live task list with state, args, result, timing, auto-refreshing via htmx
|
|
77
|
+
- **Worker management** — View worker status, pool info, active queues, configuration
|
|
78
|
+
- **Control actions** — Revoke/terminate tasks, shutdown/restart workers, manage pools and queues
|
|
79
|
+
- **Broker overview** — Queue names, routing keys, consumer counts
|
|
80
|
+
- **Django admin native** — Looks and feels like standard Django admin, no separate service to run
|
|
81
|
+
- **Database persistence** — All state persisted to database (dedicated SQLite file by default, or any Django database)
|
|
82
|
+
- **Registered tasks** — Browse all registered task types, link to filtered task list
|
|
83
|
+
|
|
84
|
+
## Task Monitoring
|
|
85
|
+
|
|
86
|
+
The task list shows all Celery tasks with color-coded states:
|
|
87
|
+
|
|
88
|
+
- **PENDING** (grey), **RECEIVED** (yellow), **STARTED** (blue), **SUCCESS** (green), **FAILURE** (red), **RETRY** (orange), **REVOKED** (purple)
|
|
89
|
+
|
|
90
|
+
Configurable columns via `TASK_COLUMNS` setting.
|
|
91
|
+
|
|
92
|
+
## Worker Management
|
|
93
|
+
|
|
94
|
+
Worker detail view with tabbed interface (matching Flower):
|
|
95
|
+
|
|
96
|
+
- **Pool** — Pool type, concurrency, processes. Controls: grow/shrink, autoscale
|
|
97
|
+
- **Queues** — Active queues. Controls: add/cancel consumer
|
|
98
|
+
- **Tasks** — Processed counts, active/scheduled/reserved/revoked tasks
|
|
99
|
+
- **Limits** — Rate limits and timeouts
|
|
100
|
+
- **Config** — Full worker Celery configuration
|
|
101
|
+
- **Stats** — System resource usage, broker connection info
|
|
102
|
+
|
|
103
|
+
## Control Actions
|
|
104
|
+
|
|
105
|
+
Full control parity with Flower:
|
|
106
|
+
|
|
107
|
+
- Revoke / terminate / abort tasks
|
|
108
|
+
- Shutdown / restart worker pool
|
|
109
|
+
- Grow / shrink pool, set autoscale
|
|
110
|
+
- Add / cancel queue consumer
|
|
111
|
+
- Set rate limits and time limits
|
|
112
|
+
|
|
113
|
+
## Configuration
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
CELERYX = {
|
|
117
|
+
"MAX_TASK_COUNT": 100_000,
|
|
118
|
+
"MAX_TASK_AGE": 86400, # 24 hours
|
|
119
|
+
"AUTO_REFRESH_INTERVAL": 3,
|
|
120
|
+
"TASK_COLUMNS": ["name", "uuid", "state", "worker", "received", "started", "runtime"],
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Unfold Theme
|
|
125
|
+
|
|
126
|
+
For [django-unfold](https://github.com/unfoldadmin/django-unfold) users:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
INSTALLED_APPS = [
|
|
130
|
+
"unfold",
|
|
131
|
+
# ...
|
|
132
|
+
"django_celeryx.unfold", # instead of django_celeryx.admin
|
|
133
|
+
]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Documentation
|
|
137
|
+
|
|
138
|
+
Full documentation at [oliverhaas.github.io/django-celeryx](https://oliverhaas.github.io/django-celeryx/)
|
|
139
|
+
|
|
140
|
+
## Requirements
|
|
141
|
+
|
|
142
|
+
- Python 3.12+
|
|
143
|
+
- Django 5.2+
|
|
144
|
+
- Celery 5.4+ or celery-asyncio 6.0+
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# django-celeryx
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/django-celeryx/)
|
|
4
|
+
[](https://pypi.org/project/django-celeryx/)
|
|
5
|
+
[](https://github.com/oliverhaas/django-celeryx/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
Celery monitoring and management for Django admin. Like Flower, but embedded in your Django admin with htmx for real-time updates.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```console
|
|
12
|
+
pip install django-celeryx[celery]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or with celery-asyncio:
|
|
16
|
+
|
|
17
|
+
```console
|
|
18
|
+
pip install django-celeryx[celery-asyncio]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
INSTALLED_APPS = [
|
|
25
|
+
# ...
|
|
26
|
+
"django_celeryx.admin",
|
|
27
|
+
]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
That's it. Start your Django server and navigate to the admin to see your Celery tasks and workers.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Real-time task monitoring** — Live task list with state, args, result, timing, auto-refreshing via htmx
|
|
35
|
+
- **Worker management** — View worker status, pool info, active queues, configuration
|
|
36
|
+
- **Control actions** — Revoke/terminate tasks, shutdown/restart workers, manage pools and queues
|
|
37
|
+
- **Broker overview** — Queue names, routing keys, consumer counts
|
|
38
|
+
- **Django admin native** — Looks and feels like standard Django admin, no separate service to run
|
|
39
|
+
- **Database persistence** — All state persisted to database (dedicated SQLite file by default, or any Django database)
|
|
40
|
+
- **Registered tasks** — Browse all registered task types, link to filtered task list
|
|
41
|
+
|
|
42
|
+
## Task Monitoring
|
|
43
|
+
|
|
44
|
+
The task list shows all Celery tasks with color-coded states:
|
|
45
|
+
|
|
46
|
+
- **PENDING** (grey), **RECEIVED** (yellow), **STARTED** (blue), **SUCCESS** (green), **FAILURE** (red), **RETRY** (orange), **REVOKED** (purple)
|
|
47
|
+
|
|
48
|
+
Configurable columns via `TASK_COLUMNS` setting.
|
|
49
|
+
|
|
50
|
+
## Worker Management
|
|
51
|
+
|
|
52
|
+
Worker detail view with tabbed interface (matching Flower):
|
|
53
|
+
|
|
54
|
+
- **Pool** — Pool type, concurrency, processes. Controls: grow/shrink, autoscale
|
|
55
|
+
- **Queues** — Active queues. Controls: add/cancel consumer
|
|
56
|
+
- **Tasks** — Processed counts, active/scheduled/reserved/revoked tasks
|
|
57
|
+
- **Limits** — Rate limits and timeouts
|
|
58
|
+
- **Config** — Full worker Celery configuration
|
|
59
|
+
- **Stats** — System resource usage, broker connection info
|
|
60
|
+
|
|
61
|
+
## Control Actions
|
|
62
|
+
|
|
63
|
+
Full control parity with Flower:
|
|
64
|
+
|
|
65
|
+
- Revoke / terminate / abort tasks
|
|
66
|
+
- Shutdown / restart worker pool
|
|
67
|
+
- Grow / shrink pool, set autoscale
|
|
68
|
+
- Add / cancel queue consumer
|
|
69
|
+
- Set rate limits and time limits
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
CELERYX = {
|
|
75
|
+
"MAX_TASK_COUNT": 100_000,
|
|
76
|
+
"MAX_TASK_AGE": 86400, # 24 hours
|
|
77
|
+
"AUTO_REFRESH_INTERVAL": 3,
|
|
78
|
+
"TASK_COLUMNS": ["name", "uuid", "state", "worker", "received", "started", "runtime"],
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Unfold Theme
|
|
83
|
+
|
|
84
|
+
For [django-unfold](https://github.com/unfoldadmin/django-unfold) users:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
INSTALLED_APPS = [
|
|
88
|
+
"unfold",
|
|
89
|
+
# ...
|
|
90
|
+
"django_celeryx.unfold", # instead of django_celeryx.admin
|
|
91
|
+
]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Documentation
|
|
95
|
+
|
|
96
|
+
Full documentation at [oliverhaas.github.io/django-celeryx](https://oliverhaas.github.io/django-celeryx/)
|
|
97
|
+
|
|
98
|
+
## Requirements
|
|
99
|
+
|
|
100
|
+
- Python 3.12+
|
|
101
|
+
- Django 5.2+
|
|
102
|
+
- Celery 5.4+ or celery-asyncio 6.0+
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Admin interface for Celery monitoring and management."""
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Django admin classes for Celery monitoring and management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from django.contrib import admin, messages
|
|
8
|
+
from django.contrib.admin.utils import unquote
|
|
9
|
+
from django.core.exceptions import PermissionDenied
|
|
10
|
+
from django.urls import path
|
|
11
|
+
from django.utils.translation import gettext_lazy as _
|
|
12
|
+
|
|
13
|
+
from .models import Dashboard, Queue, RegisteredTask, Task, Worker
|
|
14
|
+
from .queryset import (
|
|
15
|
+
QueueAdminMixin,
|
|
16
|
+
RegisteredTaskAdminMixin,
|
|
17
|
+
TaskAdminMixin,
|
|
18
|
+
WorkerAdminMixin,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from django.http import HttpRequest, HttpResponse
|
|
23
|
+
|
|
24
|
+
_TaskBase = admin.ModelAdmin[Task]
|
|
25
|
+
_WorkerBase = admin.ModelAdmin[Worker]
|
|
26
|
+
_QueueBase = admin.ModelAdmin[Queue]
|
|
27
|
+
_RegisteredTaskBase = admin.ModelAdmin[RegisteredTask]
|
|
28
|
+
else:
|
|
29
|
+
_TaskBase = admin.ModelAdmin
|
|
30
|
+
_WorkerBase = admin.ModelAdmin
|
|
31
|
+
_QueueBase = admin.ModelAdmin
|
|
32
|
+
_RegisteredTaskBase = admin.ModelAdmin
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LiveUpdateMixin:
|
|
36
|
+
"""Mixin that adds htmx live update toggle to changelist views."""
|
|
37
|
+
|
|
38
|
+
change_list_template = "admin/django_celeryx/change_list.html"
|
|
39
|
+
|
|
40
|
+
def changelist_view(
|
|
41
|
+
self,
|
|
42
|
+
request: HttpRequest,
|
|
43
|
+
extra_context: dict[str, Any] | None = None,
|
|
44
|
+
) -> HttpResponse:
|
|
45
|
+
from django_celeryx.settings import celeryx_settings
|
|
46
|
+
|
|
47
|
+
extra_context = extra_context or {}
|
|
48
|
+
|
|
49
|
+
live = request.GET.get("live") == "on"
|
|
50
|
+
extra_context["live"] = live
|
|
51
|
+
extra_context["refresh_interval"] = celeryx_settings.AUTO_REFRESH_INTERVAL
|
|
52
|
+
|
|
53
|
+
params = request.GET.copy()
|
|
54
|
+
if live:
|
|
55
|
+
params.pop("live", None)
|
|
56
|
+
else:
|
|
57
|
+
params["live"] = "on"
|
|
58
|
+
toggle_qs = params.urlencode()
|
|
59
|
+
extra_context["live_toggle_url"] = f"?{toggle_qs}" if toggle_qs else "?"
|
|
60
|
+
extra_context["live_url"] = request.get_full_path()
|
|
61
|
+
|
|
62
|
+
# Strip 'live' from request.GET so ChangeList doesn't treat it as a filter.
|
|
63
|
+
# Also update QUERY_STRING so get_full_path() stays consistent.
|
|
64
|
+
if "live" in request.GET:
|
|
65
|
+
request.GET = request.GET.copy() # type: ignore[assignment]
|
|
66
|
+
request.GET.pop("live") # type: ignore[misc]
|
|
67
|
+
request.META["QUERY_STRING"] = request.GET.urlencode()
|
|
68
|
+
|
|
69
|
+
return super().changelist_view(request, extra_context) # type: ignore[misc]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@admin.register(Task)
|
|
73
|
+
class TaskAdmin(LiveUpdateMixin, TaskAdminMixin, _TaskBase): # type: ignore[misc]
|
|
74
|
+
"""Admin for Celery tasks."""
|
|
75
|
+
|
|
76
|
+
change_list_template = "admin/django_celeryx/task/change_list.html" # type: ignore[misc]
|
|
77
|
+
actions = ["revoke_selected", "terminate_selected"] # noqa: RUF012
|
|
78
|
+
|
|
79
|
+
def has_add_permission(self, request: HttpRequest) -> bool:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def has_delete_permission(self, request: HttpRequest, obj: Task | None = None) -> bool:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
def get_urls(self) -> list:
|
|
86
|
+
urls = super().get_urls()
|
|
87
|
+
custom_urls = [
|
|
88
|
+
path(
|
|
89
|
+
"apply/",
|
|
90
|
+
self.admin_site.admin_view(self._apply_task_view),
|
|
91
|
+
name="django_celeryx_task_apply",
|
|
92
|
+
),
|
|
93
|
+
path(
|
|
94
|
+
"<path:object_id>/change/",
|
|
95
|
+
self.admin_site.admin_view(self.change_view),
|
|
96
|
+
name="django_celeryx_task_change",
|
|
97
|
+
),
|
|
98
|
+
]
|
|
99
|
+
return custom_urls + urls
|
|
100
|
+
|
|
101
|
+
def _apply_task_view(self, request: HttpRequest) -> HttpResponse:
|
|
102
|
+
from .views.apply_task import apply_task_view
|
|
103
|
+
|
|
104
|
+
return apply_task_view(request)
|
|
105
|
+
|
|
106
|
+
def change_view(
|
|
107
|
+
self,
|
|
108
|
+
request: HttpRequest,
|
|
109
|
+
object_id: str,
|
|
110
|
+
form_url: str = "",
|
|
111
|
+
extra_context: dict[str, Any] | None = None,
|
|
112
|
+
) -> HttpResponse:
|
|
113
|
+
if not self.has_view_or_change_permission(request):
|
|
114
|
+
raise PermissionDenied
|
|
115
|
+
|
|
116
|
+
from .views.task_detail import task_detail_view
|
|
117
|
+
|
|
118
|
+
return task_detail_view(request, unquote(object_id))
|
|
119
|
+
|
|
120
|
+
@admin.action(description=_("Revoke selected tasks"))
|
|
121
|
+
def revoke_selected(self, request: HttpRequest, queryset: Any) -> None:
|
|
122
|
+
from django_celeryx.control.tasks import revoke_task
|
|
123
|
+
|
|
124
|
+
count = 0
|
|
125
|
+
for task in queryset:
|
|
126
|
+
try:
|
|
127
|
+
revoke_task(task.uuid)
|
|
128
|
+
count += 1
|
|
129
|
+
except Exception as exc:
|
|
130
|
+
messages.error(request, f"Failed to revoke {task.uuid[:8]}: {exc}")
|
|
131
|
+
if count:
|
|
132
|
+
messages.success(request, f"Revoked {count} task(s).")
|
|
133
|
+
|
|
134
|
+
@admin.action(description=_("Terminate selected tasks"))
|
|
135
|
+
def terminate_selected(self, request: HttpRequest, queryset: Any) -> None:
|
|
136
|
+
from django_celeryx.control.tasks import revoke_task
|
|
137
|
+
|
|
138
|
+
count = 0
|
|
139
|
+
for task in queryset:
|
|
140
|
+
try:
|
|
141
|
+
revoke_task(task.uuid, terminate=True)
|
|
142
|
+
count += 1
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
messages.error(request, f"Failed to terminate {task.uuid[:8]}: {exc}")
|
|
145
|
+
if count:
|
|
146
|
+
messages.success(request, f"Terminated {count} task(s).")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@admin.register(Worker)
|
|
150
|
+
class WorkerAdmin(LiveUpdateMixin, WorkerAdminMixin, _WorkerBase): # type: ignore[misc]
|
|
151
|
+
"""Admin for Celery workers."""
|
|
152
|
+
|
|
153
|
+
change_list_template = "admin/django_celeryx/worker/change_list.html" # type: ignore[misc]
|
|
154
|
+
|
|
155
|
+
def has_add_permission(self, request: HttpRequest) -> bool:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
def has_delete_permission(self, request: HttpRequest, obj: Worker | None = None) -> bool:
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
def get_urls(self) -> list:
|
|
162
|
+
urls = super().get_urls()
|
|
163
|
+
custom_urls = [
|
|
164
|
+
path(
|
|
165
|
+
"<path:object_id>/change/",
|
|
166
|
+
self.admin_site.admin_view(self.change_view),
|
|
167
|
+
name="django_celeryx_worker_change",
|
|
168
|
+
),
|
|
169
|
+
]
|
|
170
|
+
return custom_urls + urls
|
|
171
|
+
|
|
172
|
+
def change_view(
|
|
173
|
+
self,
|
|
174
|
+
request: HttpRequest,
|
|
175
|
+
object_id: str,
|
|
176
|
+
form_url: str = "",
|
|
177
|
+
extra_context: dict[str, Any] | None = None,
|
|
178
|
+
) -> HttpResponse:
|
|
179
|
+
if not self.has_view_or_change_permission(request):
|
|
180
|
+
raise PermissionDenied
|
|
181
|
+
|
|
182
|
+
from .views.worker_detail import worker_detail_view
|
|
183
|
+
|
|
184
|
+
return worker_detail_view(request, unquote(object_id))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@admin.register(Queue)
|
|
188
|
+
class QueueAdmin(LiveUpdateMixin, QueueAdminMixin, _QueueBase): # type: ignore[misc]
|
|
189
|
+
"""Admin for Celery queues."""
|
|
190
|
+
|
|
191
|
+
def has_add_permission(self, request: HttpRequest) -> bool:
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
def has_delete_permission(self, request: HttpRequest, obj: Queue | None = None) -> bool:
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@admin.register(RegisteredTask)
|
|
199
|
+
class RegisteredTaskAdmin(RegisteredTaskAdminMixin, _RegisteredTaskBase): # type: ignore[misc]
|
|
200
|
+
"""Admin for registered Celery task types."""
|
|
201
|
+
|
|
202
|
+
def has_add_permission(self, request: HttpRequest) -> bool:
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
def has_delete_permission(self, request: HttpRequest, obj: RegisteredTask | None = None) -> bool:
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class DashboardPeriodFilter(admin.SimpleListFilter):
|
|
210
|
+
title = _("time period")
|
|
211
|
+
parameter_name = "period"
|
|
212
|
+
|
|
213
|
+
def lookups(self, request: HttpRequest, model_admin: admin.ModelAdmin) -> list[tuple[str, str]]:
|
|
214
|
+
return [("today", str(_("Today"))), ("7d", str(_("Last 7 days"))), ("30d", str(_("Last 30 days")))]
|
|
215
|
+
|
|
216
|
+
def queryset(self, request: HttpRequest, queryset: Any) -> Any:
|
|
217
|
+
deltas = {"today": 1, "7d": 7, "30d": 30}
|
|
218
|
+
value = self.value()
|
|
219
|
+
if value is not None and value in deltas:
|
|
220
|
+
import time
|
|
221
|
+
|
|
222
|
+
cutoff = time.time() - deltas[value] * 86400
|
|
223
|
+
return queryset.filter(updated_at__gte=cutoff)
|
|
224
|
+
return queryset
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class DashboardQueueFilter(admin.SimpleListFilter):
|
|
228
|
+
title = _("queue")
|
|
229
|
+
parameter_name = "queue"
|
|
230
|
+
|
|
231
|
+
def lookups(self, request: HttpRequest, model_admin: admin.ModelAdmin) -> list[tuple[str, str]]:
|
|
232
|
+
from django_celeryx.db_models import TaskState
|
|
233
|
+
from django_celeryx.settings import get_db_alias
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
return [
|
|
237
|
+
(q, q)
|
|
238
|
+
for q in sorted(
|
|
239
|
+
TaskState.objects.using(get_db_alias())
|
|
240
|
+
.exclude(routing_key="")
|
|
241
|
+
.values_list("routing_key", flat=True)
|
|
242
|
+
.distinct()
|
|
243
|
+
)
|
|
244
|
+
]
|
|
245
|
+
except Exception:
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
def queryset(self, request: HttpRequest, queryset: Any) -> Any:
|
|
249
|
+
if self.value():
|
|
250
|
+
return queryset.filter(routing_key=self.value())
|
|
251
|
+
return queryset
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class DashboardWorkerFilter(admin.SimpleListFilter):
|
|
255
|
+
title = _("worker")
|
|
256
|
+
parameter_name = "worker"
|
|
257
|
+
|
|
258
|
+
def lookups(self, request: HttpRequest, model_admin: admin.ModelAdmin) -> list[tuple[str, str]]:
|
|
259
|
+
from django_celeryx.db_models import TaskState
|
|
260
|
+
from django_celeryx.settings import get_db_alias
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
return [
|
|
264
|
+
(w, w)
|
|
265
|
+
for w in sorted(
|
|
266
|
+
TaskState.objects.using(get_db_alias())
|
|
267
|
+
.exclude(worker="")
|
|
268
|
+
.values_list("worker", flat=True)
|
|
269
|
+
.distinct()
|
|
270
|
+
)
|
|
271
|
+
]
|
|
272
|
+
except Exception:
|
|
273
|
+
return []
|
|
274
|
+
|
|
275
|
+
def queryset(self, request: HttpRequest, queryset: Any) -> Any:
|
|
276
|
+
if self.value():
|
|
277
|
+
return queryset.filter(worker=self.value())
|
|
278
|
+
return queryset
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if TYPE_CHECKING:
|
|
282
|
+
_DashboardBase = admin.ModelAdmin[Dashboard]
|
|
283
|
+
else:
|
|
284
|
+
_DashboardBase = admin.ModelAdmin
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@admin.register(Dashboard)
|
|
288
|
+
class DashboardAdmin(LiveUpdateMixin, _DashboardBase):
|
|
289
|
+
"""Dashboard view using native Django admin filters."""
|
|
290
|
+
|
|
291
|
+
change_list_template = "admin/django_celeryx/dashboard/change_list.html" # type: ignore[misc]
|
|
292
|
+
list_filter = [DashboardPeriodFilter, DashboardQueueFilter, DashboardWorkerFilter] # noqa: RUF012
|
|
293
|
+
show_facets = admin.ShowFacets.NEVER
|
|
294
|
+
|
|
295
|
+
def has_add_permission(self, request: HttpRequest) -> bool:
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
def has_delete_permission(self, request: HttpRequest, obj: Dashboard | None = None) -> bool:
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
def has_change_permission(self, request: HttpRequest, obj: Dashboard | None = None) -> bool:
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
def get_queryset(self, request: HttpRequest) -> Any:
|
|
305
|
+
# Return empty qs so ChangeList never queries the unmanaged Dashboard table.
|
|
306
|
+
# Actual data is computed manually in changelist_view.
|
|
307
|
+
from django_celeryx.db_models import TaskState
|
|
308
|
+
|
|
309
|
+
return TaskState.objects.none()
|
|
310
|
+
|
|
311
|
+
def changelist_view(
|
|
312
|
+
self,
|
|
313
|
+
request: HttpRequest,
|
|
314
|
+
extra_context: dict[str, Any] | None = None,
|
|
315
|
+
) -> HttpResponse:
|
|
316
|
+
extra_context = extra_context or {}
|
|
317
|
+
|
|
318
|
+
# Build a filtered queryset by applying our filters to TaskState
|
|
319
|
+
from django_celeryx.db_models import TaskState as _TaskState
|
|
320
|
+
from django_celeryx.settings import get_db_alias
|
|
321
|
+
|
|
322
|
+
from .views.dashboard import compute_dashboard_context
|
|
323
|
+
|
|
324
|
+
qs = _TaskState.objects.using(get_db_alias()).all()
|
|
325
|
+
for f_cls in self.list_filter:
|
|
326
|
+
f = f_cls(request, request.GET.copy(), _TaskState, self) # type: ignore[operator]
|
|
327
|
+
qs = f.queryset(request, qs) or qs
|
|
328
|
+
|
|
329
|
+
extra_context.update(compute_dashboard_context(qs, period=request.GET.get("period", "")))
|
|
330
|
+
|
|
331
|
+
return super().changelist_view(request, extra_context)
|