Skip to content

Commit 4b76d58

Browse files
committed
Improve sprint view
1 parent b837df1 commit 4b76d58

1 file changed

Lines changed: 96 additions & 49 deletions

File tree

pj/pj.py

Lines changed: 96 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from pathlib import Path
77
import json
88
import tomllib
9+
from collections import defaultdict
910

10-
from jira import JIRA
11+
from jira import JIRA, Issue
1112
from jira.utils import remove_empty_attributes
1213
from pydantic.dataclasses import dataclass
1314
import click
@@ -26,22 +27,12 @@ def read_config() -> Config:
2627
return Config(**data)
2728

2829

29-
def search_issues_paginated(jira: JIRA, jql: str):
30-
start_at = 0
31-
max_results = 50
32-
while results := jira.search_issues(
33-
jql,
34-
maxResults=max_results,
35-
startAt=start_at,
36-
):
37-
yield from results
38-
start_at += max_results
39-
40-
4130
class JiraContext:
4231
def __init__(self, config: Config):
4332
self._config = config
4433
self._jira: JIRA | None = None
34+
self._jira_fields: list[dict[str, t.Any]] | None = None
35+
self._sp_field_id: str | None = None
4536
self.project: str = config.project
4637

4738
@property
@@ -50,6 +41,77 @@ def jira(self) -> JIRA:
5041
self._jira = JIRA(server=self._config.server, token_auth=self._config.token)
5142
return self._jira
5243

44+
@property
45+
def jira_fields(self) -> list[dict[str, t.Any]]:
46+
if self._jira_fields is None:
47+
self._jira_fields = self.jira.fields()
48+
return self._jira_fields
49+
50+
@property
51+
def sp_field_id(self) -> str:
52+
if self._sp_field_id is None:
53+
self._sp_field_id = next(
54+
(
55+
field["id"]
56+
for field in self.jira_fields
57+
if field["name"] == "Story Points"
58+
)
59+
)
60+
return self._sp_field_id
61+
62+
def search_issues_paginated(
63+
self, jql: str, max_results: int | None = None
64+
) -> t.Iterator[Issue]:
65+
start_at = 0
66+
max_results = 50 # TODO
67+
while results := self.jira.search_issues(
68+
jql,
69+
maxResults=max_results,
70+
startAt=start_at,
71+
):
72+
yield from results
73+
start_at += max_results
74+
75+
def print_issue(self, issue) -> None:
76+
print(
77+
issue.fields.issuetype.name,
78+
"\t",
79+
issue,
80+
"\t",
81+
issue.get_field(self.sp_field_id),
82+
"\t",
83+
issue.fields.summary,
84+
)
85+
86+
def print_issue_detail(self, issue) -> None:
87+
print(issue.fields.issuetype.name, issue)
88+
print(issue.fields.summary)
89+
print(issue.fields.description)
90+
print("Status:", issue.fields.status.name)
91+
for fieldname in ["Story Points", "Resolution"]:
92+
field_id = next(
93+
(
94+
field["id"]
95+
for field in self.jira_fields
96+
if field["name"] == fieldname
97+
)
98+
)
99+
print(fieldname + ":", issue.get_field(field_id))
100+
101+
def print_kanban(self, issues) -> None:
102+
results: dict[str, list[Issue]] = defaultdict(list)
103+
sp_accumulator: dict[str, int] = defaultdict(int)
104+
for issue in issues:
105+
results[issue.fields.status.name].append(issue)
106+
sp_accumulator[issue.fields.status.name] += issue.get_field(
107+
self.sp_field_id
108+
)
109+
for status in ["New", "In Progress", "Closed"]:
110+
issues = results[status]
111+
print(f"## {status} ({sp_accumulator[status]})")
112+
for issue in issues:
113+
self.print_issue(issue)
114+
53115

54116
pass_jira_context = click.make_pass_decorator(JiraContext)
55117

@@ -62,7 +124,7 @@ def main(ctx: click.Context, /) -> None:
62124

63125

64126
@main.command()
65-
@click.option("--my/--all", default=None)
127+
@click.option("--my/--unassigned", default=None, help="defaults to all")
66128
@pass_jira_context
67129
def sprint(ctx: JiraContext, /, my: bool) -> None:
68130
conditions = [
@@ -71,44 +133,37 @@ def sprint(ctx: JiraContext, /, my: bool) -> None:
71133
]
72134
if my is True:
73135
conditions.append("assignee = currentUser()")
74-
elif my is None:
136+
elif my is False:
75137
conditions.append("assignee is EMPTY")
76-
# False -> all sprint items
138+
# None -> all sprint items
77139

78-
for issue in search_issues_paginated(
79-
ctx.jira,
80-
" AND ".join(conditions) + " ORDER BY priority DESC, updated DESC",
81-
):
82-
print(issue, issue.fields.summary)
140+
jql = " AND ".join(conditions) + " ORDER BY priority DESC, updated DESC"
141+
142+
ctx.print_kanban(ctx.search_issues_paginated(jql))
83143

84144

85145
@main.command()
86146
@pass_jira_context
87147
def issues(ctx: JiraContext, /) -> None:
88-
for issue in search_issues_paginated(
89-
ctx.jira,
90-
f"project = {ctx.project} AND resolution = Unresolved ORDER BY priority DESC, updated DESC",
91-
):
92-
print(issue, issue.fields.summary)
148+
jql = f"project = {ctx.project} AND resolution = Unresolved ORDER BY priority DESC, updated DESC"
149+
for issue in ctx.search_issues_paginated(jql):
150+
ctx.print_issue(issue)
93151

94152

95153
@main.command()
96154
@pass_jira_context
97155
def blocker(ctx: JiraContext, /) -> None:
98-
for issue in ctx.jira.search_issues(
99-
f"project = {ctx.project} AND resolution = Unresolved AND priority = blocker ORDER BY updated DESC"
100-
):
101-
print(issue, issue.fields.summary)
156+
jql = f"project = {ctx.project} AND resolution = Unresolved AND priority = blocker ORDER BY updated DESC"
157+
for issue in ctx.search_issues_paginated(jql):
158+
ctx.print_issue(issue)
102159

103160

104161
@main.command()
105162
@pass_jira_context
106163
def my_issues(ctx: JiraContext, /) -> None:
107-
jira = ctx.jira
108-
for issue in jira.search_issues(
109-
"assignee = currentUser() AND resolution = Unresolved order by updated DESC"
110-
):
111-
print(issue, issue.fields.summary)
164+
jql = "assignee = currentUser() AND resolution = Unresolved order by updated DESC"
165+
for issue in ctx.search_issues_paginated(jql):
166+
ctx.print_issue(issue)
112167

113168

114169
@main.command()
@@ -125,8 +180,8 @@ def search(
125180
search_phrase: str,
126181
) -> None:
127182
jql = f"project = {ctx.project} AND resolution = Unresolved AND text ~ '{search_phrase}' ORDER BY updated DESC"
128-
for issue in ctx.jira.search_issues(jql):
129-
print(issue, issue.fields.summary)
183+
for issue in ctx.search_issues_paginated(jql):
184+
ctx.print_issue(issue)
130185

131186

132187
@main.command()
@@ -146,20 +201,12 @@ def show(
146201
raw_issue = remove_empty_attributes(raw_issue)
147202
print(json.dumps(raw_issue))
148203
else:
149-
print(issue.fields.issuetype.name, issue)
150-
print(issue.fields.summary)
151-
print(issue.fields.description)
152-
print("Status:", issue.fields.status.name)
153-
fields = ctx.jira.fields()
154-
for fieldname in ["Story Points", "Resolution"]:
155-
field_id = next(
156-
(field["id"] for field in fields if field["name"] == fieldname)
157-
)
158-
print(fieldname + ":", issue.get_field(field_id))
204+
ctx.print_issue_detail(issue)
159205

160206

161207
@main.command()
162-
@click.option("--assign/--no-assign", default=True)
208+
@click.option("--assign/--no-assign", default=False)
209+
# @click.option("--task/--bug/--epic")
163210
@click.argument("summary")
164211
@click.argument("description")
165212
@pass_jira_context
@@ -179,7 +226,7 @@ def create(
179226
if assign:
180227
fields["assignee"] = {"name": ctx.jira.current_user()}
181228
issue = ctx.jira.create_issue(fields)
182-
print(issue)
229+
ctx.print_issue_detail(issue)
183230

184231

185232
@main.command()

0 commit comments

Comments
 (0)