heinzel-issues 0.2.0__py3-none-any.whl
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.
- heinzel_issues/__init__.py +251 -0
- heinzel_issues/_beads.py +513 -0
- heinzel_issues/_contract.py +110 -0
- heinzel_issues/_releases.py +190 -0
- heinzel_issues/_service.py +541 -0
- heinzel_issues/py.typed +0 -0
- heinzel_issues/views/index.js +186 -0
- heinzel_issues/views/style.css +30 -0
- heinzel_issues-0.2.0.dist-info/METADATA +131 -0
- heinzel_issues-0.2.0.dist-info/RECORD +13 -0
- heinzel_issues-0.2.0.dist-info/WHEEL +4 -0
- heinzel_issues-0.2.0.dist-info/entry_points.txt +2 -0
- heinzel_issues-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Issues: report a problem or request something, and see whether it is received, being worked on or done.
|
|
2
|
+
|
|
3
|
+
Behind it is any tracker with a connector for the task-tracking contract (`heinzel_issues._contract`); a local Beads
|
|
4
|
+
database is built in, written through the methods of Heinzel's `Tracker` contract (ADR 0141). It registers no
|
|
5
|
+
capability adapter, so installing it never changes which tracker other plugins use. The tracker's statuses, ids and
|
|
6
|
+
workflow stay behind the three plain states; the release that shipped a fix is read from local git clones. Off until
|
|
7
|
+
a tracker the person can reach, such as the team's management repository, is set up.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from heinzel import (
|
|
13
|
+
Check,
|
|
14
|
+
ConfigEditor,
|
|
15
|
+
HeinzelError,
|
|
16
|
+
Kernel,
|
|
17
|
+
ListEntry,
|
|
18
|
+
ListWidget,
|
|
19
|
+
Module,
|
|
20
|
+
Navigate,
|
|
21
|
+
Notifications,
|
|
22
|
+
Prompter,
|
|
23
|
+
RunOperation,
|
|
24
|
+
Status,
|
|
25
|
+
)
|
|
26
|
+
from pydantic import JsonValue
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
from rich.text import Text
|
|
29
|
+
|
|
30
|
+
from heinzel_issues._beads import BeadsBoard, BeadsSettings, BeadsTracker, state_of
|
|
31
|
+
from heinzel_issues._contract import (
|
|
32
|
+
ENTRY_POINTS,
|
|
33
|
+
STATE_LABELS,
|
|
34
|
+
Attachment,
|
|
35
|
+
Task,
|
|
36
|
+
TaskState,
|
|
37
|
+
TaskTracker,
|
|
38
|
+
TrackerError,
|
|
39
|
+
)
|
|
40
|
+
from heinzel_issues._releases import Release, ReleaseResolver, ReleaseSource, released_text
|
|
41
|
+
from heinzel_issues._service import (
|
|
42
|
+
NONE,
|
|
43
|
+
NOT_SET_UP,
|
|
44
|
+
ROUTE,
|
|
45
|
+
SETUP,
|
|
46
|
+
IssuesService,
|
|
47
|
+
IssuesSettings,
|
|
48
|
+
Report,
|
|
49
|
+
Reports,
|
|
50
|
+
Step,
|
|
51
|
+
Watched,
|
|
52
|
+
upload,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
module = Module("issues", "Report a problem or request something, and see when it is done and released.")
|
|
56
|
+
module.settings(IssuesSettings)
|
|
57
|
+
module.service(IssuesService)
|
|
58
|
+
module.nav(title="Issues", icon="inbox", sidebar=Navigate(path=ROUTE), order=650)
|
|
59
|
+
module.action("requests", label="Issues", icon="inbox", effect=Navigate(path=ROUTE), order=650)
|
|
60
|
+
module.view(
|
|
61
|
+
Path(__file__).parent / "views",
|
|
62
|
+
abi=1,
|
|
63
|
+
name="requests",
|
|
64
|
+
source="list",
|
|
65
|
+
title="Issues",
|
|
66
|
+
fallback="Issues cannot be shown here.",
|
|
67
|
+
order=650,
|
|
68
|
+
refresh=60,
|
|
69
|
+
actions={
|
|
70
|
+
"connect": RunOperation(operation="connect", inputs=("path", "labels", "releases")),
|
|
71
|
+
"send": RunOperation(operation="submit", inputs=("text", "file_name", "file_data")),
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _render(result: Reports) -> Table | Text:
|
|
77
|
+
if not result.ready:
|
|
78
|
+
problem = str(result.problem)
|
|
79
|
+
return Text(f"{problem} Run {result.fix}." if result.fix and result.fix not in problem else problem)
|
|
80
|
+
if not result.reports:
|
|
81
|
+
return Text('Nothing reported yet. Send something with hz issues submit "what happened".')
|
|
82
|
+
table = Table("Report or request", "Status", "Sent")
|
|
83
|
+
for report in result.reports:
|
|
84
|
+
table.add_row(Text(report.title), Text(report.status), f"{report.submitted_at:%d %b %Y}")
|
|
85
|
+
return table
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _render_one(report: Report) -> Text:
|
|
89
|
+
return Text(f"{report.title}\n{report.status}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _fail(error: TrackerError) -> HeinzelError:
|
|
93
|
+
return HeinzelError(str(error))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@module.operation(name="list", private=True, render=_render, audience="everyday")
|
|
97
|
+
def list_(service: IssuesService) -> Reports:
|
|
98
|
+
"""Show what you reported or requested: received, being worked on, or done and in which release."""
|
|
99
|
+
return service.overview()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@module.operation(read_only=False, private=True, render=_render_one, audience="everyday")
|
|
103
|
+
def submit(
|
|
104
|
+
service: IssuesService,
|
|
105
|
+
text: str,
|
|
106
|
+
attachment: list[Path] | None = None,
|
|
107
|
+
file_name: str = "",
|
|
108
|
+
file_data: str = "",
|
|
109
|
+
) -> Report:
|
|
110
|
+
"""Report a problem or request something; optional attachments are PNG, JPEG or text files.
|
|
111
|
+
|
|
112
|
+
The app sends one attached file as its name and base64 content (`file_name`, `file_data`); in a terminal,
|
|
113
|
+
name files with `--attachment`.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
HeinzelError: If it cannot be sent; the message says why.
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
service.refresh()
|
|
120
|
+
uploads = [upload(file_name, file_data)] if file_data else []
|
|
121
|
+
board = BeadsBoard(service.settings.beads) if service.settings.tracker == "beads" else None
|
|
122
|
+
return service.submit(text, attachment or [], board=board, uploads=uploads)
|
|
123
|
+
except TrackerError as error:
|
|
124
|
+
raise _fail(error) from None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@module.operation(read_only=False, private=True, render=_render)
|
|
128
|
+
def connect(service: IssuesService, editor: ConfigEditor, path: str, labels: str = "", releases: str = "") -> Reports:
|
|
129
|
+
"""Set up issue tracking: send reports and requests to the Beads database in a folder on this computer.
|
|
130
|
+
|
|
131
|
+
`labels` are all the further labels for every new report (empty: none), and `releases` are git clones whose
|
|
132
|
+
tags are releases (empty: keep what is set); both are lists separated by commas.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
HeinzelError: If the folder has no Beads database or it does not answer; nothing is saved then.
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
return service.connect(editor, path, labels, releases)
|
|
139
|
+
except TrackerError as error:
|
|
140
|
+
raise _fail(error) from None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@module.operation(private=True, render=_render_one)
|
|
144
|
+
def show(service: IssuesService, id: str) -> Report: # noqa: A002 - the tracker's word for it
|
|
145
|
+
"""Show one of your reports or requests by its tracker id, with the tracker's details.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
HeinzelError: If it cannot be read.
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
return service.get(id)
|
|
152
|
+
except TrackerError as error:
|
|
153
|
+
raise _fail(error) from None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@module.job(schedule="*/5 * * * *", name="watch", timeout="2m")
|
|
157
|
+
def watch(service: IssuesService, kernel: Kernel) -> JsonValue:
|
|
158
|
+
"""Every five minutes, notice when a report is being worked on, is done, or is released."""
|
|
159
|
+
notifications = kernel.resolve(Notifications) if kernel.provides(Notifications) else None
|
|
160
|
+
try:
|
|
161
|
+
return service.watch(notifications).model_dump(mode="json")
|
|
162
|
+
except TrackerError as error:
|
|
163
|
+
raise _fail(error) from None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@module.widget(source="list", order=650)
|
|
167
|
+
def today(result: Reports) -> ListWidget | None:
|
|
168
|
+
"""Your open reports and requests on Today; nothing while issue tracking is off or all is done."""
|
|
169
|
+
if not result.ready:
|
|
170
|
+
return None
|
|
171
|
+
open_ = [report for report in result.reports if report.state != TaskState.DONE]
|
|
172
|
+
if not open_:
|
|
173
|
+
return None
|
|
174
|
+
return ListWidget(
|
|
175
|
+
title="Your reports and requests",
|
|
176
|
+
items=[
|
|
177
|
+
ListEntry(
|
|
178
|
+
text=report.title,
|
|
179
|
+
detail=report.status,
|
|
180
|
+
tone="ok" if report.state == TaskState.WORKING else "neutral",
|
|
181
|
+
effect=Navigate(path=ROUTE),
|
|
182
|
+
)
|
|
183
|
+
for report in open_[:8]
|
|
184
|
+
],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@module.health
|
|
189
|
+
def tracking(service: IssuesService) -> list[Check]:
|
|
190
|
+
"""Off is fine; a chosen tracker must answer."""
|
|
191
|
+
if not service.enabled:
|
|
192
|
+
return [Check(name="tracker", status=Status.UP, detail="off; set it up on the Issues page")]
|
|
193
|
+
result = service.overview()
|
|
194
|
+
if not result.ready:
|
|
195
|
+
return [Check(name="tracker", status=Status.DOWN, detail=str(result.problem), fix=SETUP)]
|
|
196
|
+
detail = f"{service.settings.tracker}: {len(result.reports)} reports and requests"
|
|
197
|
+
return [Check(name="tracker", status=Status.UP, detail=detail)]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@module.setup(required=False)
|
|
201
|
+
def setup(settings: IssuesSettings, prompter: Prompter, editor: ConfigEditor) -> None:
|
|
202
|
+
"""Choose where reports and requests go, and where releases are found."""
|
|
203
|
+
choice = prompter.choice("Where should reports and requests go?", [NONE, "beads"], default=settings.tracker)
|
|
204
|
+
if choice == NONE:
|
|
205
|
+
editor.write("issues.tracker", NONE)
|
|
206
|
+
prompter.say("Issue tracking is off.")
|
|
207
|
+
return
|
|
208
|
+
folder = settings.beads.path or Path.cwd()
|
|
209
|
+
while True:
|
|
210
|
+
answer = prompter.text("The folder with the .beads database", default=str(folder))
|
|
211
|
+
folder = Path(answer).expanduser()
|
|
212
|
+
if (folder / ".beads").is_dir():
|
|
213
|
+
break
|
|
214
|
+
prompter.say(f"{folder} has no .beads database; run bd init there first, or pick another folder.")
|
|
215
|
+
clones = [str(source.path) for source in settings.releases]
|
|
216
|
+
clone = prompter.text("A git clone whose tags are releases, empty to skip", default=clones[0] if clones else "")
|
|
217
|
+
if not prompter.confirm(f"Send reports and requests to the Beads database in {folder}?"):
|
|
218
|
+
prompter.say("Nothing was saved.")
|
|
219
|
+
return
|
|
220
|
+
editor.write("issues.beads", {**settings.beads.model_dump(mode="json"), "path": str(folder)})
|
|
221
|
+
if clone:
|
|
222
|
+
editor.write("issues.releases", [{"path": str(Path(clone).expanduser())}])
|
|
223
|
+
path = editor.write("issues.tracker", "beads")
|
|
224
|
+
prompter.say(f"Saved to {path}.")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
__all__ = [
|
|
228
|
+
"ENTRY_POINTS",
|
|
229
|
+
"NOT_SET_UP",
|
|
230
|
+
"STATE_LABELS",
|
|
231
|
+
"Attachment",
|
|
232
|
+
"BeadsBoard",
|
|
233
|
+
"BeadsSettings",
|
|
234
|
+
"BeadsTracker",
|
|
235
|
+
"IssuesService",
|
|
236
|
+
"IssuesSettings",
|
|
237
|
+
"Release",
|
|
238
|
+
"ReleaseResolver",
|
|
239
|
+
"ReleaseSource",
|
|
240
|
+
"Report",
|
|
241
|
+
"Reports",
|
|
242
|
+
"Step",
|
|
243
|
+
"Task",
|
|
244
|
+
"TaskState",
|
|
245
|
+
"TaskTracker",
|
|
246
|
+
"TrackerError",
|
|
247
|
+
"Watched",
|
|
248
|
+
"module",
|
|
249
|
+
"released_text",
|
|
250
|
+
"state_of",
|
|
251
|
+
]
|