queue-watch 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.
Files changed (35) hide show
  1. queue_watch-0.1.0/.gitignore +7 -0
  2. queue_watch-0.1.0/LICENSE +21 -0
  3. queue_watch-0.1.0/PKG-INFO +141 -0
  4. queue_watch-0.1.0/README.md +114 -0
  5. queue_watch-0.1.0/examples/queue-status.py +64 -0
  6. queue_watch-0.1.0/examples/queue.json +5 -0
  7. queue_watch-0.1.0/examples/sources.toml +8 -0
  8. queue_watch-0.1.0/pyproject.toml +58 -0
  9. queue_watch-0.1.0/src/queue_watch/__init__.py +3 -0
  10. queue_watch-0.1.0/src/queue_watch/adapters/__init__.py +34 -0
  11. queue_watch-0.1.0/src/queue_watch/adapters/command.py +54 -0
  12. queue_watch-0.1.0/src/queue_watch/adapters/flock_queue.py +322 -0
  13. queue_watch-0.1.0/src/queue_watch/adapters/github_actions.py +164 -0
  14. queue_watch-0.1.0/src/queue_watch/adapters/github_merge_queue.py +141 -0
  15. queue_watch-0.1.0/src/queue_watch/app.py +876 -0
  16. queue_watch-0.1.0/src/queue_watch/changes.py +181 -0
  17. queue_watch-0.1.0/src/queue_watch/cli.py +129 -0
  18. queue_watch-0.1.0/src/queue_watch/config.py +105 -0
  19. queue_watch-0.1.0/src/queue_watch/discovery.py +149 -0
  20. queue_watch-0.1.0/src/queue_watch/model.py +188 -0
  21. queue_watch-0.1.0/src/queue_watch/process.py +67 -0
  22. queue_watch-0.1.0/src/queue_watch/widgets.py +774 -0
  23. queue_watch-0.1.0/tests/__snapshots__/test_snapshots/test_comfortable_density.raw +257 -0
  24. queue_watch-0.1.0/tests/__snapshots__/test_snapshots/test_layout[narrow].raw +195 -0
  25. queue_watch-0.1.0/tests/__snapshots__/test_snapshots/test_layout[short].raw +158 -0
  26. queue_watch-0.1.0/tests/__snapshots__/test_snapshots/test_layout[wide].raw +257 -0
  27. queue_watch-0.1.0/tests/test_app.py +369 -0
  28. queue_watch-0.1.0/tests/test_changes.py +83 -0
  29. queue_watch-0.1.0/tests/test_command.py +93 -0
  30. queue_watch-0.1.0/tests/test_contract.py +90 -0
  31. queue_watch-0.1.0/tests/test_discovery.py +81 -0
  32. queue_watch-0.1.0/tests/test_flock_queue.py +153 -0
  33. queue_watch-0.1.0/tests/test_github_actions.py +121 -0
  34. queue_watch-0.1.0/tests/test_github_merge_queue.py +79 -0
  35. queue_watch-0.1.0/tests/test_snapshots.py +79 -0
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ dist/
6
+ .ruff_cache/
7
+ snapshot_report.html
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wolfgang Schoenberger
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,141 @@
1
+ Metadata-Version: 2.5
2
+ Name: queue-watch
3
+ Version: 0.1.0
4
+ Summary: Read-only terminal dashboard for CI runs and merge queues, with mouse support
5
+ Project-URL: Homepage, https://github.com/wolfiesch/queue-watch
6
+ Project-URL: Issues, https://github.com/wolfiesch/queue-watch/issues
7
+ Project-URL: Changelog, https://github.com/wolfiesch/queue-watch/releases
8
+ Author: Wolfgang Schoenberger
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ci,dashboard,github-actions,merge-queue,textual,tui
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Build Tools
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: textual<7,>=6
26
+ Description-Content-Type: text/markdown
27
+
28
+ # queue-watch
29
+
30
+ Read-only terminal dashboard for CI runs and merge queues. It joins records from every source into one row per change, so a branch's CI runs and its merge-queue entry read together, and draws each merge queue as a chain of chips in queue order. queue-watch never reruns, cancels, merges or takes a lock.
31
+
32
+ ![queue-watch showing a merge queue of four changes, the Changes table grouped by state, and the inspector for a failed change](https://raw.githubusercontent.com/wolfiesch/queue-watch/main/docs/screenshot.png)
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ uv tool install queue-watch # or: pipx install queue-watch
38
+ uvx queue-watch # run once without installing
39
+ ```
40
+
41
+ Requires Python 3.11+. The GitHub adapters use an authenticated [GitHub CLI](https://cli.github.com/) (`gh auth login`).
42
+
43
+ ## Use
44
+
45
+ ```sh
46
+ queue-watch # inside a GitHub checkout
47
+ queue-watch --repo owner/repo # Actions plus the merge queue, if any
48
+ queue-watch --profile work # ~/.config/queue-watch/work.toml
49
+ queue-watch --config sources.toml # any combination of sources
50
+ queue-watch --repo owner/repo --once # one JSON snapshot, no TUI
51
+ queue-watch --theme nord --ascii # any Textual theme; ASCII glyphs only
52
+ queue-watch --density comfortable # title, owner and PR on a second line
53
+ ```
54
+
55
+ With no flags, queue-watch looks for a `.queue-watch.toml` at the root of the current Git checkout. Without one, it watches the checkout's GitHub repository: Actions runs, plus the merge queue when the default branch has one. `--profile NAME` reads `$XDG_CONFIG_HOME/queue-watch/NAME.toml` (default `~/.config`).
56
+
57
+ The Changes tab groups changes into Needs attention, Running, Queued and Done; every view a source reports also gets its own tab. Done starts folded while any other group has changes; press enter on a group row, or click it, to fold or unfold it. Select a change or a queue chip to see its CI checks, the jobs of the most notable one, its queue position and ETA, and recent history in the inspector.
58
+
59
+ Each merge queue gets a strip above the tabs: its entries in merge order, when it should clear, merges in the last hour, and how long recent attempts took. Queue depth over the session appears as a sparkline once two fetches have answered; sources report only the current queue, so the history starts when queue-watch does. On terminals under 30 rows the entries shrink to one line each.
60
+
61
+ Keyboard: arrows to move, `1` to `9` for tabs, `/` to search (esc to clear), `o` to open the selection in a browser, `c` to copy its SHA, `d` to switch row density, `r` to refresh, `p` to pause, `q` to quit. The mouse works too: click tabs, rows, chips and the inspector buttons, and scroll with the wheel. When a source fails, its last good data stays on screen and its status pill turns stale.
62
+
63
+ ### Repository configs need trust
64
+
65
+ A config can run commands and open SSH connections, so a `.queue-watch.toml` that arrives with a clone does nothing until you review it and run:
66
+
67
+ ```sh
68
+ queue-watch trust
69
+ ```
70
+
71
+ Trust covers the file's exact bytes (SHA-256, stored in `$XDG_STATE_HOME/queue-watch/trusted.json`). After any edit, queue-watch refuses the file and names the command to run again. Files passed with `--config` or `--profile` are yours and load without this step.
72
+
73
+ ## Sources
74
+
75
+ A config file lists sources as `[[source]]` tables. `id` and `adapter` are required; `label` and `interval` (seconds) are optional; every other key is an adapter option.
76
+
77
+ ```toml
78
+ [[source]]
79
+ id = "ci"
80
+ adapter = "github-actions"
81
+ repo = "owner/repo"
82
+
83
+ [[source]]
84
+ id = "merge-queue"
85
+ adapter = "github-merge-queue"
86
+ repo = "owner/repo"
87
+ branch = "main" # optional; default branch otherwise
88
+
89
+ [[source]]
90
+ id = "deploys"
91
+ adapter = "command"
92
+ command = ["./bin/deploy-status", "--json"]
93
+ timeout = 20 # optional seconds, default 25
94
+
95
+ [[source]]
96
+ id = "merges"
97
+ adapter = "flock-queue"
98
+ host = "build-host" # optional SSH destination
99
+ root = "~/src/project" # Git checkout that defines {git_dir}
100
+ lock = "{git_dir}/merge.lock"
101
+ tickets = "{git_dir}/merge.queue/*.ticket"
102
+ process = "merge.py"
103
+ owner_flag = "--owner"
104
+ history = "{git_dir}/merge-history.jsonl"
105
+ history_columns = [{ key = "fields.duration_seconds", label = "Took", format = "seconds" }]
106
+ ```
107
+
108
+ | Adapter | Watches | Options |
109
+ |---|---|---|
110
+ | `github-actions` | Workflow runs, with jobs and steps in the inspector | `repo` |
111
+ | `github-merge-queue` | GitHub's native merge queue: position, state, author and GitHub's merge estimate per pull request | `repo`, `branch` |
112
+ | `command` | Anything else: a program that prints one snapshot | `command`, `host`, `cwd`, `timeout` |
113
+ | `flock-queue` | A Linux FIFO of flock-held ticket files guarding one merge lock, plus an optional JSON Lines attempt log | `lock`, `tickets`, `process`, `root`, `host`, `interpreters`, `owner_flag`, `history`, `history_limit`, `history_columns` |
114
+
115
+ Merge queue states map as `QUEUED` and `MERGEABLE` to queued, `AWAITING_CHECKS` and `LOCKED` to running, and `UNMERGEABLE` to blocked. A branch without a queue shows a warning instead of an error.
116
+
117
+ `flock-queue` reads lock holders from `/proc/locks` rather than acquiring them. With `host` set, the probe goes to `python3 -` over SSH on stdin and nothing is installed on the host.
118
+
119
+ ### Command sources
120
+
121
+ Use the `command` adapter for GitLab, Buildkite, Jenkins, Bors or an in-house queue. `command` is an argument list run without a shell. Locally it runs in `cwd`, which defaults to the config file's directory; relative paths resolve against that directory too. With `host` set, it runs over SSH (`BatchMode`, every argument quoted for the remote shell), in `cwd` on the host when given.
122
+
123
+ The program must exit zero and print one snapshot (below) on stdout within `timeout` seconds. Anything else, including a snapshot that breaks the contract, shows as a source error while the last good data stays on screen. [`examples/`](https://github.com/wolfiesch/queue-watch/tree/main/examples) has a working script and config.
124
+
125
+ ## Snapshot contract
126
+
127
+ Every adapter emits the same versioned snapshot (`queue-watch.snapshot/1`): a list of records with `id`, `kind`, `state` and `title`, plus optional `ref`, `sha`, `owner`, `position`, timestamps, `url`, `fields` and `children`. `state` is one of `queued`, `running`, `blocked`, `success`, `failure`, `cancelled`, `neutral` or `unknown`; the adapter's own term is kept in `raw_state`. Records that share a `ref` or `sha` describe the same change across sources. [`model.py`](https://github.com/wolfiesch/queue-watch/blob/main/src/queue_watch/model.py) defines and validates the contract, and `--once` prints it.
128
+
129
+ ## Development
130
+
131
+ ```sh
132
+ uv sync
133
+ uv run pytest -q # includes SVG snapshot tests
134
+ uv run pytest -q --snapshot-update # after an intended layout change
135
+ ```
136
+
137
+ `docs/screenshot.png` is the wide layout snapshot rendered in a browser. To release, bump `__version__` in `src/queue_watch/__init__.py`, merge, and push a matching `vX.Y.Z` tag; the release workflow tests, builds, publishes to PyPI with trusted publishing, and creates the GitHub release.
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,114 @@
1
+ # queue-watch
2
+
3
+ Read-only terminal dashboard for CI runs and merge queues. It joins records from every source into one row per change, so a branch's CI runs and its merge-queue entry read together, and draws each merge queue as a chain of chips in queue order. queue-watch never reruns, cancels, merges or takes a lock.
4
+
5
+ ![queue-watch showing a merge queue of four changes, the Changes table grouped by state, and the inspector for a failed change](https://raw.githubusercontent.com/wolfiesch/queue-watch/main/docs/screenshot.png)
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ uv tool install queue-watch # or: pipx install queue-watch
11
+ uvx queue-watch # run once without installing
12
+ ```
13
+
14
+ Requires Python 3.11+. The GitHub adapters use an authenticated [GitHub CLI](https://cli.github.com/) (`gh auth login`).
15
+
16
+ ## Use
17
+
18
+ ```sh
19
+ queue-watch # inside a GitHub checkout
20
+ queue-watch --repo owner/repo # Actions plus the merge queue, if any
21
+ queue-watch --profile work # ~/.config/queue-watch/work.toml
22
+ queue-watch --config sources.toml # any combination of sources
23
+ queue-watch --repo owner/repo --once # one JSON snapshot, no TUI
24
+ queue-watch --theme nord --ascii # any Textual theme; ASCII glyphs only
25
+ queue-watch --density comfortable # title, owner and PR on a second line
26
+ ```
27
+
28
+ With no flags, queue-watch looks for a `.queue-watch.toml` at the root of the current Git checkout. Without one, it watches the checkout's GitHub repository: Actions runs, plus the merge queue when the default branch has one. `--profile NAME` reads `$XDG_CONFIG_HOME/queue-watch/NAME.toml` (default `~/.config`).
29
+
30
+ The Changes tab groups changes into Needs attention, Running, Queued and Done; every view a source reports also gets its own tab. Done starts folded while any other group has changes; press enter on a group row, or click it, to fold or unfold it. Select a change or a queue chip to see its CI checks, the jobs of the most notable one, its queue position and ETA, and recent history in the inspector.
31
+
32
+ Each merge queue gets a strip above the tabs: its entries in merge order, when it should clear, merges in the last hour, and how long recent attempts took. Queue depth over the session appears as a sparkline once two fetches have answered; sources report only the current queue, so the history starts when queue-watch does. On terminals under 30 rows the entries shrink to one line each.
33
+
34
+ Keyboard: arrows to move, `1` to `9` for tabs, `/` to search (esc to clear), `o` to open the selection in a browser, `c` to copy its SHA, `d` to switch row density, `r` to refresh, `p` to pause, `q` to quit. The mouse works too: click tabs, rows, chips and the inspector buttons, and scroll with the wheel. When a source fails, its last good data stays on screen and its status pill turns stale.
35
+
36
+ ### Repository configs need trust
37
+
38
+ A config can run commands and open SSH connections, so a `.queue-watch.toml` that arrives with a clone does nothing until you review it and run:
39
+
40
+ ```sh
41
+ queue-watch trust
42
+ ```
43
+
44
+ Trust covers the file's exact bytes (SHA-256, stored in `$XDG_STATE_HOME/queue-watch/trusted.json`). After any edit, queue-watch refuses the file and names the command to run again. Files passed with `--config` or `--profile` are yours and load without this step.
45
+
46
+ ## Sources
47
+
48
+ A config file lists sources as `[[source]]` tables. `id` and `adapter` are required; `label` and `interval` (seconds) are optional; every other key is an adapter option.
49
+
50
+ ```toml
51
+ [[source]]
52
+ id = "ci"
53
+ adapter = "github-actions"
54
+ repo = "owner/repo"
55
+
56
+ [[source]]
57
+ id = "merge-queue"
58
+ adapter = "github-merge-queue"
59
+ repo = "owner/repo"
60
+ branch = "main" # optional; default branch otherwise
61
+
62
+ [[source]]
63
+ id = "deploys"
64
+ adapter = "command"
65
+ command = ["./bin/deploy-status", "--json"]
66
+ timeout = 20 # optional seconds, default 25
67
+
68
+ [[source]]
69
+ id = "merges"
70
+ adapter = "flock-queue"
71
+ host = "build-host" # optional SSH destination
72
+ root = "~/src/project" # Git checkout that defines {git_dir}
73
+ lock = "{git_dir}/merge.lock"
74
+ tickets = "{git_dir}/merge.queue/*.ticket"
75
+ process = "merge.py"
76
+ owner_flag = "--owner"
77
+ history = "{git_dir}/merge-history.jsonl"
78
+ history_columns = [{ key = "fields.duration_seconds", label = "Took", format = "seconds" }]
79
+ ```
80
+
81
+ | Adapter | Watches | Options |
82
+ |---|---|---|
83
+ | `github-actions` | Workflow runs, with jobs and steps in the inspector | `repo` |
84
+ | `github-merge-queue` | GitHub's native merge queue: position, state, author and GitHub's merge estimate per pull request | `repo`, `branch` |
85
+ | `command` | Anything else: a program that prints one snapshot | `command`, `host`, `cwd`, `timeout` |
86
+ | `flock-queue` | A Linux FIFO of flock-held ticket files guarding one merge lock, plus an optional JSON Lines attempt log | `lock`, `tickets`, `process`, `root`, `host`, `interpreters`, `owner_flag`, `history`, `history_limit`, `history_columns` |
87
+
88
+ Merge queue states map as `QUEUED` and `MERGEABLE` to queued, `AWAITING_CHECKS` and `LOCKED` to running, and `UNMERGEABLE` to blocked. A branch without a queue shows a warning instead of an error.
89
+
90
+ `flock-queue` reads lock holders from `/proc/locks` rather than acquiring them. With `host` set, the probe goes to `python3 -` over SSH on stdin and nothing is installed on the host.
91
+
92
+ ### Command sources
93
+
94
+ Use the `command` adapter for GitLab, Buildkite, Jenkins, Bors or an in-house queue. `command` is an argument list run without a shell. Locally it runs in `cwd`, which defaults to the config file's directory; relative paths resolve against that directory too. With `host` set, it runs over SSH (`BatchMode`, every argument quoted for the remote shell), in `cwd` on the host when given.
95
+
96
+ The program must exit zero and print one snapshot (below) on stdout within `timeout` seconds. Anything else, including a snapshot that breaks the contract, shows as a source error while the last good data stays on screen. [`examples/`](https://github.com/wolfiesch/queue-watch/tree/main/examples) has a working script and config.
97
+
98
+ ## Snapshot contract
99
+
100
+ Every adapter emits the same versioned snapshot (`queue-watch.snapshot/1`): a list of records with `id`, `kind`, `state` and `title`, plus optional `ref`, `sha`, `owner`, `position`, timestamps, `url`, `fields` and `children`. `state` is one of `queued`, `running`, `blocked`, `success`, `failure`, `cancelled`, `neutral` or `unknown`; the adapter's own term is kept in `raw_state`. Records that share a `ref` or `sha` describe the same change across sources. [`model.py`](https://github.com/wolfiesch/queue-watch/blob/main/src/queue_watch/model.py) defines and validates the contract, and `--once` prints it.
101
+
102
+ ## Development
103
+
104
+ ```sh
105
+ uv sync
106
+ uv run pytest -q # includes SVG snapshot tests
107
+ uv run pytest -q --snapshot-update # after an intended layout change
108
+ ```
109
+
110
+ `docs/screenshot.png` is the wide layout snapshot rendered in a browser. To release, bump `__version__` in `src/queue_watch/__init__.py`, merge, and push a matching `vX.Y.Z` tag; the release workflow tests, builds, publishes to PyPI with trusted publishing, and creates the GitHub release.
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+ """Example source for the ``command`` adapter: a queue kept as a JSON file.
3
+
4
+ queue-watch runs this with no shell and reads one snapshot v1 object from
5
+ stdout. Adapt the ``load`` function to whatever your system exposes: an HTTP
6
+ API, a database query, a directory of ticket files.
7
+
8
+ [[source]]
9
+ id = "deploys"
10
+ adapter = "command"
11
+ command = ["python3", "examples/queue-status.py", "examples/queue.json"]
12
+ """
13
+
14
+ import json
15
+ import sys
16
+ from datetime import UTC, datetime
17
+
18
+ STATES = {"waiting": "queued", "deploying": "running", "held": "blocked"}
19
+
20
+
21
+ def load(path: str) -> list[dict]:
22
+ with open(path) as handle:
23
+ return json.load(handle)
24
+
25
+
26
+ def main() -> None:
27
+ entries = load(sys.argv[1])
28
+ records = [
29
+ {
30
+ "id": str(entry["id"]),
31
+ "kind": "queue_entry",
32
+ "state": STATES.get(entry["status"], "unknown"),
33
+ "raw_state": entry["status"],
34
+ "title": entry["title"],
35
+ "owner": entry.get("owner"),
36
+ "position": position,
37
+ "created_at": entry.get("since"),
38
+ "fields": {"target": entry.get("target")},
39
+ }
40
+ for position, entry in enumerate(entries, start=1)
41
+ ]
42
+ snapshot = {
43
+ "schema": "queue-watch.snapshot/1",
44
+ "observed_at": datetime.now(UTC).isoformat(),
45
+ "records": records,
46
+ "views": [
47
+ {
48
+ "kind": "queue_entry",
49
+ "label": "Deploys",
50
+ "columns": [
51
+ {"key": "position", "label": "#", "width": 4},
52
+ {"key": "state", "label": "State", "width": 12, "format": "state"},
53
+ {"key": "title", "label": "Change", "width": 36},
54
+ {"key": "fields.target", "label": "Target", "width": 12},
55
+ {"key": "created_at", "label": "Waiting", "width": 9, "format": "age"},
56
+ ],
57
+ }
58
+ ],
59
+ }
60
+ json.dump(snapshot, sys.stdout)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()
@@ -0,0 +1,5 @@
1
+ [
2
+ {"id": 41, "status": "deploying", "title": "Rotate API gateway certificates", "owner": "dana", "since": "2026-10-20T16:02:00+00:00", "target": "prod-eu"},
3
+ {"id": 42, "status": "waiting", "title": "Raise worker pool to 24", "owner": "lee", "since": "2026-10-20T16:10:00+00:00", "target": "prod-us"},
4
+ {"id": 43, "status": "held", "title": "Schema change: orders.index", "owner": "sam", "since": "2026-10-20T16:12:00+00:00", "target": "prod-us"}
5
+ ]
@@ -0,0 +1,8 @@
1
+ # queue-watch --config examples/sources.toml
2
+ # Paths in `command` and `cwd` are relative to this file's directory.
3
+
4
+ [[source]]
5
+ id = "deploys"
6
+ adapter = "command"
7
+ label = "Deploys"
8
+ command = ["python3", "queue-status.py", "queue.json"]
@@ -0,0 +1,58 @@
1
+ [project]
2
+ name = "queue-watch"
3
+ dynamic = ["version"]
4
+ description = "Read-only terminal dashboard for CI runs and merge queues, with mouse support"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Wolfgang Schoenberger" }]
9
+ requires-python = ">=3.11"
10
+ dependencies = ["textual>=6,<7"]
11
+ keywords = ["ci", "merge-queue", "github-actions", "tui", "textual", "dashboard"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: MacOS",
17
+ "Operating System :: POSIX :: Linux",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Build Tools",
24
+ "Topic :: Software Development :: Testing",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/wolfiesch/queue-watch"
29
+ Issues = "https://github.com/wolfiesch/queue-watch/issues"
30
+ Changelog = "https://github.com/wolfiesch/queue-watch/releases"
31
+
32
+ [project.scripts]
33
+ queue-watch = "queue_watch.cli:main"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pytest>=8",
38
+ "pytest-textual-snapshot>=1.0.0",
39
+ ]
40
+
41
+ [build-system]
42
+ requires = ["hatchling>=1.27"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [tool.hatch.version]
46
+ path = "src/queue_watch/__init__.py"
47
+
48
+ [tool.hatch.build.targets.sdist]
49
+ include = ["/src", "/tests", "/examples", "/README.md", "/LICENSE"]
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
53
+
54
+ [tool.ruff]
55
+ line-length = 100
56
+
57
+ [tool.ruff.lint]
58
+ extend-select = ["I", "B", "UP"]
@@ -0,0 +1,3 @@
1
+ """Read-only terminal dashboard for CI runs and merge queues."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,34 @@
1
+ """Adapters turn one external system into snapshot v1 (see queue_watch.model)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import ClassVar
6
+
7
+
8
+ class Adapter:
9
+ """Base class. Subclasses take their options as keyword-only arguments.
10
+
11
+ A ``config_dir`` parameter, when present, receives the directory of the config
12
+ file; it is supplied by the loader and cannot be set from configuration.
13
+ """
14
+
15
+ name: ClassVar[str]
16
+ label: ClassVar[str]
17
+ interval: ClassVar[float] = 15.0
18
+
19
+ async def snapshot(self) -> dict:
20
+ raise NotImplementedError
21
+
22
+ async def children(self, record: dict) -> list[dict]:
23
+ """Detail records (jobs, steps) for the inspector; inline by default."""
24
+ return record.get("children") or []
25
+
26
+
27
+ def registry() -> dict[str, type[Adapter]]:
28
+ from .command import Command
29
+ from .flock_queue import FlockQueue
30
+ from .github_actions import GitHubActions
31
+ from .github_merge_queue import GitHubMergeQueue
32
+
33
+ adapters = (GitHubActions, GitHubMergeQueue, FlockQueue, Command)
34
+ return {adapter.name: adapter for adapter in adapters}
@@ -0,0 +1,54 @@
1
+ """Any program that prints one snapshot v1 JSON object on stdout.
2
+
3
+ This is the extension point for systems without a built-in adapter (GitLab,
4
+ Buildkite, Jenkins, Bors, in-house queues). ``command`` is an argument list run
5
+ without a shell. Locally it runs in ``cwd``, which defaults to the directory of
6
+ the config file. With ``host`` set it runs over SSH, in ``cwd`` on that host
7
+ when given. A nonzero exit, a timeout or output that breaks the contract is
8
+ shown as a source error, and the last good data stays on screen.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from pathlib import Path
15
+
16
+ from ..process import check_host, over_ssh, run_json
17
+ from . import Adapter
18
+
19
+
20
+ class Command(Adapter):
21
+ name = "command"
22
+ label = "Command"
23
+ interval = 15.0
24
+
25
+ def __init__(
26
+ self,
27
+ *,
28
+ command: list[str],
29
+ host: str | None = None,
30
+ cwd: str | None = None,
31
+ timeout: float = 25,
32
+ config_dir: Path | None = None,
33
+ ):
34
+ if (
35
+ not isinstance(command, list)
36
+ or not command
37
+ or not all(isinstance(arg, str) and arg for arg in command)
38
+ ):
39
+ raise ValueError(
40
+ 'command must be a list of arguments, for example ["./status", "--json"]'
41
+ )
42
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or timeout <= 0:
43
+ raise ValueError("timeout must be a positive number of seconds")
44
+ self.command, self.host, self.timeout = command, check_host(host), float(timeout)
45
+ if self.host is not None:
46
+ self.cwd = cwd
47
+ else:
48
+ base = config_dir or Path.cwd()
49
+ self.cwd = str(base / os.path.expanduser(cwd)) if cwd else str(base)
50
+
51
+ async def snapshot(self) -> dict:
52
+ if self.host is not None:
53
+ return await run_json(over_ssh(self.host, self.command, self.cwd), timeout=self.timeout)
54
+ return await run_json(self.command, cwd=self.cwd, timeout=self.timeout)