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.
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ dump.rdb
@@ -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.
@@ -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
+ ![Overview](docs/overview.png)
41
+
42
+ | Queue | Cron jobs |
43
+ | --- | --- |
44
+ | ![Failed jobs](docs/queue.png) | ![Cron jobs](docs/cron.png) |
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
+ ![Overview](docs/overview.png)
8
+
9
+ | Queue | Cron jobs |
10
+ | --- | --- |
11
+ | ![Failed jobs](docs/queue.png) | ![Cron jobs](docs/cron.png) |
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()