saq-board 0.1.0__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.
- saq_board-0.1.0/.gitignore +9 -0
- saq_board-0.1.0/LICENSE +22 -0
- saq_board-0.1.0/PKG-INFO +186 -0
- saq_board-0.1.0/README.md +153 -0
- saq_board-0.1.0/THIRD_PARTY_NOTICES.md +44 -0
- saq_board-0.1.0/pyproject.toml +49 -0
- saq_board-0.1.0/saq_board/__init__.py +33 -0
- saq_board-0.1.0/saq_board/__main__.py +84 -0
- saq_board-0.1.0/saq_board/api.py +313 -0
- saq_board-0.1.0/saq_board/cron.py +177 -0
- saq_board-0.1.0/saq_board/store.py +243 -0
- saq_board-0.1.0/saq_board/web/__init__.py +0 -0
- saq_board-0.1.0/saq_board/web/aiohttp.py +84 -0
- saq_board-0.1.0/saq_board/web/common.py +42 -0
- saq_board-0.1.0/saq_board/web/starlette.py +72 -0
- saq_board-0.1.0/saq_board/web/static/app.js +936 -0
- saq_board-0.1.0/saq_board/web/static/board.css +549 -0
- saq_board-0.1.0/saq_board/web/static/favicon.svg +1 -0
- saq_board-0.1.0/saq_board/web/static/fonts/OFL.txt +93 -0
- saq_board-0.1.0/saq_board/web/static/fonts/geist-mono.woff2 +0 -0
- saq_board-0.1.0/saq_board/web/static/fonts/geist.woff2 +0 -0
- saq_board-0.1.0/saq_board/web/static/vendor/preact-htm.js +2 -0
- saq_board-0.1.0/saq_board/worker.py +112 -0
saq_board-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rkwap (https://github.com/rkwap)
|
|
4
|
+
Copyright (c) 2021 Toby Mao, for the parts adapted from SAQ's dashboard (https://github.com/tobymao/saq)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
saq_board-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: saq-board
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A bull-board style dashboard for SAQ, with Sidekiq-cron style cron jobs
|
|
5
|
+
Project-URL: Homepage, https://github.com/rkwap/saq-board
|
|
6
|
+
Author: rkwap
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
10
|
+
License-File: saq_board/web/static/fonts/OFL.txt
|
|
11
|
+
Keywords: cron,dashboard,queue,redis,saq
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: System :: Monitoring
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Requires-Dist: croniter>=1.0
|
|
17
|
+
Requires-Dist: saq[redis]>=0.24.13
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: aiohttp; extra == 'dev'
|
|
20
|
+
Requires-Dist: aiohttp-basicauth; extra == 'dev'
|
|
21
|
+
Requires-Dist: httpx; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-aiohttp; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
26
|
+
Requires-Dist: starlette; extra == 'dev'
|
|
27
|
+
Provides-Extra: starlette
|
|
28
|
+
Requires-Dist: starlette; extra == 'starlette'
|
|
29
|
+
Provides-Extra: web
|
|
30
|
+
Requires-Dist: aiohttp; extra == 'web'
|
|
31
|
+
Requires-Dist: aiohttp-basicauth; extra == 'web'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# SAQ Board
|
|
35
|
+
|
|
36
|
+
A dashboard for [SAQ](https://github.com/tobymao/saq) inspired by [bull-board](https://github.com/felixmosh/bull-board).
|
|
37
|
+
|
|
38
|
+
It extends SAQ's built-in dashboard and its API is a superset of SAQ's, so it drops in wherever you use `saq.web` today.
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
| Queue | Cron jobs |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
|  |  |
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **Queues**: overview with live counts, per-queue tabs for active, queued, scheduled, completed, failed and aborted jobs, plus workers.
|
|
49
|
+
- **Jobs**: bull-board style cards with a timeline, data, result, error and options tabs, progress bars, and retry, abort, run now (promote), duplicate and remove actions.
|
|
50
|
+
- **Bulk actions**: retry all, clean all, promote all, abort all.
|
|
51
|
+
- **Pause and resume** a queue.
|
|
52
|
+
- **Add jobs** from the dashboard, picking from the functions your workers register.
|
|
53
|
+
- **Cron jobs**, Sidekiq-cron style: defined in code, then enabled, disabled, enqueued now or deleted from the dashboard, with next run, last enqueued and history.
|
|
54
|
+
- Light and dark theme, adjustable refresh, relative or absolute times, local or UTC, read-only mode, Redis stats.
|
|
55
|
+
- Preact + htm with no build step, like SAQ's own dashboard. Fonts and scripts ship with the package, so it works offline.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
pip install "saq-board[starlette]" # Starlette / FastAPI
|
|
61
|
+
pip install "saq-board[web]" # aiohttp and the saq-board command
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Redis queues only for now.
|
|
65
|
+
|
|
66
|
+
## 1. Add the plugin to your workers
|
|
67
|
+
|
|
68
|
+
SAQ doesn't keep finished jobs, can't pause a queue and schedules cron jobs in code only. The plugin adds all three. Wrap your worker settings:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from saq import Queue
|
|
72
|
+
from saq_board import CronJob, with_board
|
|
73
|
+
|
|
74
|
+
queue = Queue.from_url("redis://localhost")
|
|
75
|
+
|
|
76
|
+
settings = with_board({
|
|
77
|
+
"queue": queue,
|
|
78
|
+
"functions": [send_email],
|
|
79
|
+
"cron_jobs": [
|
|
80
|
+
CronJob(cleanup, cron="0 * * * *", description="Hourly cleanup"),
|
|
81
|
+
],
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Run workers exactly as before: `saq app.worker.settings`.
|
|
86
|
+
|
|
87
|
+
`with_board(settings, history_limit=1000, cron_poll=1.0)`:
|
|
88
|
+
|
|
89
|
+
- `history_limit`: finished jobs kept per status (complete, failed, aborted). The oldest are dropped first.
|
|
90
|
+
- `cron_poll`: seconds between cron checks.
|
|
91
|
+
|
|
92
|
+
## 2. Run the dashboard
|
|
93
|
+
|
|
94
|
+
**Starlette / FastAPI**, a drop-in for `saq.web.starlette.saq_web`:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from starlette.routing import Mount
|
|
98
|
+
from saq_board import saq_board
|
|
99
|
+
|
|
100
|
+
routes = [Mount("/monitor", saq_board("/monitor", queues=[queue]))]
|
|
101
|
+
# FastAPI: app.mount("/monitor", saq_board("/monitor", queues=[queue]))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**aiohttp**, a drop-in for `saq.web.aiohttp.create_app`:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from saq_board import create_app
|
|
108
|
+
|
|
109
|
+
app.add_subapp("/monitor", create_app([queue], root_path="/monitor"))
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**On its own**:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
saq-board --url redis://localhost:6379 # queues registered by the plugin
|
|
116
|
+
saq-board --url redis://localhost:6379 -q default -q emails
|
|
117
|
+
saq-board --settings app.worker.settings --port 8080
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
All of them take `read_only=True` (`--read-only`), which hides every action and rejects them server-side.
|
|
121
|
+
|
|
122
|
+
### Authentication
|
|
123
|
+
|
|
124
|
+
Same as SAQ's dashboard. The aiohttp app and the `saq-board` command turn on basic auth when `AUTH_PASSWORD` is set (user `AUTH_USER`, default `admin`); this needs `aiohttp_basicauth`, which the `web` extra installs. With Starlette or FastAPI, protect the mount with your own middleware or dependencies.
|
|
125
|
+
|
|
126
|
+
## Cron jobs
|
|
127
|
+
|
|
128
|
+
Cron jobs work like Sidekiq-cron:
|
|
129
|
+
|
|
130
|
+
- They are defined in code (`cron_jobs` in the worker settings) and synced to Redis when a worker starts.
|
|
131
|
+
- The dashboard enables, disables, enqueues now or deletes them. Enabled or disabled survives restarts; a deleted job comes back the next time a worker that defines it starts.
|
|
132
|
+
- Every worker process checks the schedule, and a claim in Redis makes sure each run is enqueued once.
|
|
133
|
+
- A run that is more than 60 seconds late (for example, no worker was up) is skipped, not caught up.
|
|
134
|
+
- `unique=True` (the default, as in SAQ) skips a run while the previous one is still queued or running.
|
|
135
|
+
- Times follow the worker's `cron_tz` (UTC by default).
|
|
136
|
+
|
|
137
|
+
`saq_board.CronJob` is SAQ's `CronJob` plus `name`, `description` and `enabled` (the initial state). Plain `saq.CronJob` works too and is named after its function. Give jobs that share a function different names.
|
|
138
|
+
|
|
139
|
+
## API
|
|
140
|
+
|
|
141
|
+
Everything SAQ's dashboard serves, plus:
|
|
142
|
+
|
|
143
|
+
| Method | Path | |
|
|
144
|
+
| --- | --- | --- |
|
|
145
|
+
| GET | `/api/queues` | Queues with counts, totals, paused flag and workers |
|
|
146
|
+
| GET | `/api/queues/{queue}` | One queue, as in SAQ, plus the registered functions |
|
|
147
|
+
| GET | `/api/queues/{queue}/jobs?status=&offset=&limit=` | Jobs by status |
|
|
148
|
+
| POST | `/api/queues/{queue}/jobs` | Add a job: `{"function", "kwargs", "options"}` |
|
|
149
|
+
| GET | `/api/queues/{queue}/jobs/{job}` | One job |
|
|
150
|
+
| POST | `/api/queues/{queue}/jobs/{job}/{action}` | `retry`, `abort`, `promote`, `remove` |
|
|
151
|
+
| POST | `/api/queues/{queue}/{action}` | `pause`, `resume`, `promote-all`, and with `{"status"}`: `clean`, `retry-all`, `abort-all` |
|
|
152
|
+
| GET | `/api/redis` | Redis stats |
|
|
153
|
+
| GET | `/api/cron` | Cron jobs |
|
|
154
|
+
| GET | `/api/cron/{queue}/{name}` | One cron job with its history |
|
|
155
|
+
| POST | `/api/cron/{queue}/{name}/{action}` | `enqueue`, `enable`, `disable`, `delete` |
|
|
156
|
+
| POST | `/api/cron/{action}-all` | The same for every cron job |
|
|
157
|
+
|
|
158
|
+
## How it works
|
|
159
|
+
|
|
160
|
+
The plugin wraps the worker's queue: `finish` also records the job in a capped history, and `dequeue` waits while the queue is paused (a pause takes effect within a second). It also moves `cron_jobs` to its own scheduler, so SAQ's scheduler doesn't run them too, and tags the worker's metadata so the dashboard can tell whether workers run the plugin.
|
|
161
|
+
|
|
162
|
+
Board data lives under each queue's SAQ namespace, `saq:<queue>:board:*`, so it goes away with the queue.
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]" uvicorn
|
|
168
|
+
redis-server --daemonize yes
|
|
169
|
+
.venv/bin/pytest # uses redis://localhost:6379/15, override with SAQ_BOARD_TEST_REDIS
|
|
170
|
+
.venv/bin/python examples/demo.py # http://localhost:8000/monitor
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Credits
|
|
174
|
+
|
|
175
|
+
Built by [rkwap](https://github.com/rkwap).
|
|
176
|
+
|
|
177
|
+
SAQ Board stands on the shoulders of:
|
|
178
|
+
|
|
179
|
+
- [SAQ](https://github.com/tobymao/saq) by [Toby Mao](https://github.com/tobymao) (MIT): the queue this extends, whose dashboard API, structure and `#1095c1` accent it keeps.
|
|
180
|
+
- [bull-board](https://github.com/felixmosh/bull-board) by [felixmosh](https://github.com/felixmosh) (MIT): the dashboard design it follows.
|
|
181
|
+
- [Sidekiq-cron](https://github.com/sidekiq-cron/sidekiq-cron) by [ondrejbartas](https://github.com/ondrejbartas) and contributors (MIT): how cron jobs behave and look.
|
|
182
|
+
- [Preact](https://preactjs.com) (MIT) and [htm](https://github.com/developit/htm) (Apache-2.0) by Jason Miller, [Feather icons](https://feathericons.com) by Cole Bemis (MIT), and the [Geist](https://vercel.com/font) fonts (SIL OFL 1.1), all bundled. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
[MIT](LICENSE), like SAQ and bull-board.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# SAQ Board
|
|
2
|
+
|
|
3
|
+
A dashboard for [SAQ](https://github.com/tobymao/saq) inspired by [bull-board](https://github.com/felixmosh/bull-board).
|
|
4
|
+
|
|
5
|
+
It extends SAQ's built-in dashboard and its API is a superset of SAQ's, so it drops in wherever you use `saq.web` today.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
| Queue | Cron jobs |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
|  |  |
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Queues**: overview with live counts, per-queue tabs for active, queued, scheduled, completed, failed and aborted jobs, plus workers.
|
|
16
|
+
- **Jobs**: bull-board style cards with a timeline, data, result, error and options tabs, progress bars, and retry, abort, run now (promote), duplicate and remove actions.
|
|
17
|
+
- **Bulk actions**: retry all, clean all, promote all, abort all.
|
|
18
|
+
- **Pause and resume** a queue.
|
|
19
|
+
- **Add jobs** from the dashboard, picking from the functions your workers register.
|
|
20
|
+
- **Cron jobs**, Sidekiq-cron style: defined in code, then enabled, disabled, enqueued now or deleted from the dashboard, with next run, last enqueued and history.
|
|
21
|
+
- Light and dark theme, adjustable refresh, relative or absolute times, local or UTC, read-only mode, Redis stats.
|
|
22
|
+
- Preact + htm with no build step, like SAQ's own dashboard. Fonts and scripts ship with the package, so it works offline.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install "saq-board[starlette]" # Starlette / FastAPI
|
|
28
|
+
pip install "saq-board[web]" # aiohttp and the saq-board command
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Redis queues only for now.
|
|
32
|
+
|
|
33
|
+
## 1. Add the plugin to your workers
|
|
34
|
+
|
|
35
|
+
SAQ doesn't keep finished jobs, can't pause a queue and schedules cron jobs in code only. The plugin adds all three. Wrap your worker settings:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from saq import Queue
|
|
39
|
+
from saq_board import CronJob, with_board
|
|
40
|
+
|
|
41
|
+
queue = Queue.from_url("redis://localhost")
|
|
42
|
+
|
|
43
|
+
settings = with_board({
|
|
44
|
+
"queue": queue,
|
|
45
|
+
"functions": [send_email],
|
|
46
|
+
"cron_jobs": [
|
|
47
|
+
CronJob(cleanup, cron="0 * * * *", description="Hourly cleanup"),
|
|
48
|
+
],
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Run workers exactly as before: `saq app.worker.settings`.
|
|
53
|
+
|
|
54
|
+
`with_board(settings, history_limit=1000, cron_poll=1.0)`:
|
|
55
|
+
|
|
56
|
+
- `history_limit`: finished jobs kept per status (complete, failed, aborted). The oldest are dropped first.
|
|
57
|
+
- `cron_poll`: seconds between cron checks.
|
|
58
|
+
|
|
59
|
+
## 2. Run the dashboard
|
|
60
|
+
|
|
61
|
+
**Starlette / FastAPI**, a drop-in for `saq.web.starlette.saq_web`:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from starlette.routing import Mount
|
|
65
|
+
from saq_board import saq_board
|
|
66
|
+
|
|
67
|
+
routes = [Mount("/monitor", saq_board("/monitor", queues=[queue]))]
|
|
68
|
+
# FastAPI: app.mount("/monitor", saq_board("/monitor", queues=[queue]))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**aiohttp**, a drop-in for `saq.web.aiohttp.create_app`:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from saq_board import create_app
|
|
75
|
+
|
|
76
|
+
app.add_subapp("/monitor", create_app([queue], root_path="/monitor"))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**On its own**:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
saq-board --url redis://localhost:6379 # queues registered by the plugin
|
|
83
|
+
saq-board --url redis://localhost:6379 -q default -q emails
|
|
84
|
+
saq-board --settings app.worker.settings --port 8080
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
All of them take `read_only=True` (`--read-only`), which hides every action and rejects them server-side.
|
|
88
|
+
|
|
89
|
+
### Authentication
|
|
90
|
+
|
|
91
|
+
Same as SAQ's dashboard. The aiohttp app and the `saq-board` command turn on basic auth when `AUTH_PASSWORD` is set (user `AUTH_USER`, default `admin`); this needs `aiohttp_basicauth`, which the `web` extra installs. With Starlette or FastAPI, protect the mount with your own middleware or dependencies.
|
|
92
|
+
|
|
93
|
+
## Cron jobs
|
|
94
|
+
|
|
95
|
+
Cron jobs work like Sidekiq-cron:
|
|
96
|
+
|
|
97
|
+
- They are defined in code (`cron_jobs` in the worker settings) and synced to Redis when a worker starts.
|
|
98
|
+
- The dashboard enables, disables, enqueues now or deletes them. Enabled or disabled survives restarts; a deleted job comes back the next time a worker that defines it starts.
|
|
99
|
+
- Every worker process checks the schedule, and a claim in Redis makes sure each run is enqueued once.
|
|
100
|
+
- A run that is more than 60 seconds late (for example, no worker was up) is skipped, not caught up.
|
|
101
|
+
- `unique=True` (the default, as in SAQ) skips a run while the previous one is still queued or running.
|
|
102
|
+
- Times follow the worker's `cron_tz` (UTC by default).
|
|
103
|
+
|
|
104
|
+
`saq_board.CronJob` is SAQ's `CronJob` plus `name`, `description` and `enabled` (the initial state). Plain `saq.CronJob` works too and is named after its function. Give jobs that share a function different names.
|
|
105
|
+
|
|
106
|
+
## API
|
|
107
|
+
|
|
108
|
+
Everything SAQ's dashboard serves, plus:
|
|
109
|
+
|
|
110
|
+
| Method | Path | |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| GET | `/api/queues` | Queues with counts, totals, paused flag and workers |
|
|
113
|
+
| GET | `/api/queues/{queue}` | One queue, as in SAQ, plus the registered functions |
|
|
114
|
+
| GET | `/api/queues/{queue}/jobs?status=&offset=&limit=` | Jobs by status |
|
|
115
|
+
| POST | `/api/queues/{queue}/jobs` | Add a job: `{"function", "kwargs", "options"}` |
|
|
116
|
+
| GET | `/api/queues/{queue}/jobs/{job}` | One job |
|
|
117
|
+
| POST | `/api/queues/{queue}/jobs/{job}/{action}` | `retry`, `abort`, `promote`, `remove` |
|
|
118
|
+
| POST | `/api/queues/{queue}/{action}` | `pause`, `resume`, `promote-all`, and with `{"status"}`: `clean`, `retry-all`, `abort-all` |
|
|
119
|
+
| GET | `/api/redis` | Redis stats |
|
|
120
|
+
| GET | `/api/cron` | Cron jobs |
|
|
121
|
+
| GET | `/api/cron/{queue}/{name}` | One cron job with its history |
|
|
122
|
+
| POST | `/api/cron/{queue}/{name}/{action}` | `enqueue`, `enable`, `disable`, `delete` |
|
|
123
|
+
| POST | `/api/cron/{action}-all` | The same for every cron job |
|
|
124
|
+
|
|
125
|
+
## How it works
|
|
126
|
+
|
|
127
|
+
The plugin wraps the worker's queue: `finish` also records the job in a capped history, and `dequeue` waits while the queue is paused (a pause takes effect within a second). It also moves `cron_jobs` to its own scheduler, so SAQ's scheduler doesn't run them too, and tags the worker's metadata so the dashboard can tell whether workers run the plugin.
|
|
128
|
+
|
|
129
|
+
Board data lives under each queue's SAQ namespace, `saq:<queue>:board:*`, so it goes away with the queue.
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]" uvicorn
|
|
135
|
+
redis-server --daemonize yes
|
|
136
|
+
.venv/bin/pytest # uses redis://localhost:6379/15, override with SAQ_BOARD_TEST_REDIS
|
|
137
|
+
.venv/bin/python examples/demo.py # http://localhost:8000/monitor
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Credits
|
|
141
|
+
|
|
142
|
+
Built by [rkwap](https://github.com/rkwap).
|
|
143
|
+
|
|
144
|
+
SAQ Board stands on the shoulders of:
|
|
145
|
+
|
|
146
|
+
- [SAQ](https://github.com/tobymao/saq) by [Toby Mao](https://github.com/tobymao) (MIT): the queue this extends, whose dashboard API, structure and `#1095c1` accent it keeps.
|
|
147
|
+
- [bull-board](https://github.com/felixmosh/bull-board) by [felixmosh](https://github.com/felixmosh) (MIT): the dashboard design it follows.
|
|
148
|
+
- [Sidekiq-cron](https://github.com/sidekiq-cron/sidekiq-cron) by [ondrejbartas](https://github.com/ondrejbartas) and contributors (MIT): how cron jobs behave and look.
|
|
149
|
+
- [Preact](https://preactjs.com) (MIT) and [htm](https://github.com/developit/htm) (Apache-2.0) by Jason Miller, [Feather icons](https://feathericons.com) by Cole Bemis (MIT), and the [Geist](https://vercel.com/font) fonts (SIL OFL 1.1), all bundled. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
[MIT](LICENSE), like SAQ and bull-board.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
SAQ Board ships these third-party files in `saq_board/web/static/`.
|
|
4
|
+
|
|
5
|
+
## Preact 10 (in `vendor/preact-htm.js`)
|
|
6
|
+
|
|
7
|
+
MIT License. Copyright (c) 2015-present Jason Miller. https://github.com/preactjs/preact
|
|
8
|
+
|
|
9
|
+
## htm 3.1.1 (in `vendor/preact-htm.js`)
|
|
10
|
+
|
|
11
|
+
Apache License, Version 2.0. Copyright 2018 Google Inc. https://github.com/developit/htm
|
|
12
|
+
|
|
13
|
+
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
14
|
+
The file is the unmodified `htm/preact/standalone.module.js` build, with a header comment added.
|
|
15
|
+
|
|
16
|
+
## Feather icons (paths inlined in `app.js`)
|
|
17
|
+
|
|
18
|
+
MIT License. Copyright (c) 2013-2023 Cole Bemis. https://github.com/feathericons/feather
|
|
19
|
+
|
|
20
|
+
## Geist and Geist Mono (in `fonts/`)
|
|
21
|
+
|
|
22
|
+
SIL Open Font License, Version 1.1. Copyright 2024 The Geist Project Authors. https://github.com/vercel/geist-font
|
|
23
|
+
|
|
24
|
+
The full license is in `saq_board/web/static/fonts/OFL.txt`.
|
|
25
|
+
|
|
26
|
+
## MIT License text
|
|
27
|
+
|
|
28
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
29
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
30
|
+
in the Software without restriction, including without limitation the rights
|
|
31
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
32
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
33
|
+
furnished to do so, subject to the following conditions:
|
|
34
|
+
|
|
35
|
+
The above copyright notice and this permission notice shall be included in all
|
|
36
|
+
copies or substantial portions of the Software.
|
|
37
|
+
|
|
38
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
39
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
40
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
41
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
42
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
43
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
44
|
+
SOFTWARE.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "saq-board"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A bull-board style dashboard for SAQ, with Sidekiq-cron style cron jobs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md", "saq_board/web/static/fonts/OFL.txt"]
|
|
12
|
+
authors = [{ name = "rkwap" }]
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
dependencies = ["saq[redis]>=0.24.13", "croniter>=1.0"]
|
|
15
|
+
keywords = ["saq", "queue", "dashboard", "cron", "redis"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Framework :: AsyncIO",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: System :: Monitoring",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
starlette = ["starlette"]
|
|
24
|
+
web = ["aiohttp", "aiohttp_basicauth"]
|
|
25
|
+
dev = ["saq-board[starlette,web]", "httpx", "pytest", "pytest-asyncio", "pytest-aiohttp", "ruff"]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
saq-board = "saq_board.__main__:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/rkwap/saq-board"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.version]
|
|
34
|
+
path = "saq_board/__init__.py"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.sdist]
|
|
37
|
+
include = ["saq_board", "README.md", "LICENSE", "THIRD_PARTY_NOTICES.md"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
asyncio_mode = "auto"
|
|
41
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 110
|
|
45
|
+
target-version = "py39"
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
49
|
+
ignore = ["UP007", "UP045"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SAQ Board: a bull-board style dashboard for SAQ with Sidekiq-cron style cron jobs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import typing as t
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
from saq_board.cron import CronJob # noqa: E402
|
|
12
|
+
from saq_board.worker import track, with_board # noqa: E402
|
|
13
|
+
|
|
14
|
+
if t.TYPE_CHECKING:
|
|
15
|
+
from aiohttp.web import Application
|
|
16
|
+
from saq.queue import Queue
|
|
17
|
+
from starlette.applications import Starlette
|
|
18
|
+
|
|
19
|
+
__all__ = ["CronJob", "__version__", "create_app", "saq_board", "track", "with_board"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def saq_board(root_path: str, queues: list[Queue], **kwargs: t.Any) -> Starlette:
|
|
23
|
+
"""Starlette/FastAPI app, a drop-in for ``saq.web.starlette.saq_web``."""
|
|
24
|
+
from saq_board.web.starlette import saq_board as app
|
|
25
|
+
|
|
26
|
+
return app(root_path, queues, **kwargs)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_app(queues: list[Queue], **kwargs: t.Any) -> Application:
|
|
30
|
+
"""aiohttp app, a drop-in for ``saq.web.aiohttp.create_app``."""
|
|
31
|
+
from saq_board.web.aiohttp import create_app as app
|
|
32
|
+
|
|
33
|
+
return app(queues, **kwargs)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
saq-board: run the dashboard on its own.
|
|
3
|
+
|
|
4
|
+
saq-board --url redis://localhost:6379 --queue default --queue emails
|
|
5
|
+
saq-board --settings app.worker.settings
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from saq.queue import Queue
|
|
17
|
+
|
|
18
|
+
from saq_board.store import REGISTRY, text
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def discover(url: str) -> list[str]:
|
|
22
|
+
"""Queue names registered by the worker plugin, else found by scanning for SAQ keys."""
|
|
23
|
+
from redis import asyncio as aioredis
|
|
24
|
+
|
|
25
|
+
async def run() -> list[str]:
|
|
26
|
+
redis = aioredis.from_url(url)
|
|
27
|
+
try:
|
|
28
|
+
names = {text(n) for n in await redis.smembers(REGISTRY)}
|
|
29
|
+
if not names:
|
|
30
|
+
async for key in redis.scan_iter(match="saq:*:incomplete", count=1000):
|
|
31
|
+
names.add(text(key)[len("saq:") : -len(":incomplete")])
|
|
32
|
+
return sorted(names)
|
|
33
|
+
finally:
|
|
34
|
+
await redis.aclose()
|
|
35
|
+
|
|
36
|
+
return asyncio.run(run())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> None:
|
|
40
|
+
parser = argparse.ArgumentParser(description="SAQ Board dashboard")
|
|
41
|
+
parser.add_argument("--url", default=os.environ.get("SAQ_BOARD_URL"),
|
|
42
|
+
help="Redis url, eg: redis://localhost:6379 (env SAQ_BOARD_URL)")
|
|
43
|
+
parser.add_argument("--queue", "-q", action="append", default=[],
|
|
44
|
+
help="Queue name to monitor, repeatable. Default: discover them")
|
|
45
|
+
parser.add_argument("--settings", "-s", action="append", default=[],
|
|
46
|
+
help="SAQ worker settings whose queue to monitor, eg: app.worker.settings")
|
|
47
|
+
parser.add_argument("--host", default="0.0.0.0", help="Host to bind, defaults to 0.0.0.0")
|
|
48
|
+
parser.add_argument("--port", type=int, default=8080, help="Port, defaults to 8080")
|
|
49
|
+
parser.add_argument("--root-path", default="", help="Path prefix when behind a proxy")
|
|
50
|
+
parser.add_argument("--read-only", action="store_true", help="Hide and reject every action")
|
|
51
|
+
args = parser.parse_args()
|
|
52
|
+
|
|
53
|
+
logging.basicConfig(level=logging.INFO)
|
|
54
|
+
sys.path.append(os.getcwd())
|
|
55
|
+
|
|
56
|
+
queues: list[Queue] = []
|
|
57
|
+
if args.settings:
|
|
58
|
+
from saq.worker import import_settings
|
|
59
|
+
|
|
60
|
+
queues += [import_settings(s).get("queue") or Queue.from_url("redis://localhost")
|
|
61
|
+
for s in args.settings]
|
|
62
|
+
if args.url:
|
|
63
|
+
names = args.queue or discover(args.url) or ["default"]
|
|
64
|
+
queues += [Queue.from_url(args.url, name=name) for name in names]
|
|
65
|
+
if not queues:
|
|
66
|
+
parser.error("pass --url or --settings")
|
|
67
|
+
|
|
68
|
+
from aiohttp import web
|
|
69
|
+
|
|
70
|
+
from saq_board.web.aiohttp import create_app
|
|
71
|
+
|
|
72
|
+
app = create_app(queues, root_path=args.root_path, read_only=args.read_only)
|
|
73
|
+
|
|
74
|
+
async def disconnect(_app: web.Application) -> None:
|
|
75
|
+
for queue in queues:
|
|
76
|
+
await queue.disconnect()
|
|
77
|
+
|
|
78
|
+
app.on_shutdown.append(disconnect)
|
|
79
|
+
logging.info("Monitoring queues: %s", ", ".join(q.name for q in queues))
|
|
80
|
+
web.run_app(app, host=args.host, port=args.port)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
main()
|