Skip to content

Commit 90c230e

Browse files
committed
Add pj
1 parent a5ded49 commit 90c230e

3 files changed

Lines changed: 246 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
__pycache__/
2+
.mypy_cache/

pj/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.jiraauth

pj/pj.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#!/bin/env python3
2+
3+
# Reference: https://jira.readthedocs.io
4+
5+
import typing as t
6+
from pathlib import Path
7+
import json
8+
import tomllib
9+
10+
from jira import JIRA
11+
from jira.utils import remove_empty_attributes
12+
from pydantic.dataclasses import dataclass
13+
import click
14+
15+
16+
@dataclass
17+
class Config:
18+
server: str = "https://issues.redhat.com"
19+
token: str = ""
20+
project: str = "PULP"
21+
22+
23+
def read_config() -> Config:
24+
conf_path = Path(".") / ".jiraauth"
25+
data = tomllib.loads(conf_path.read_text())["default"]
26+
return Config(**data)
27+
28+
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+
41+
@click.group()
42+
@click.pass_context
43+
def main(ctx: click.Context) -> None:
44+
config = read_config()
45+
jira = JIRA(server=config.server, token_auth=config.token)
46+
ctx.obj = {
47+
"jira": jira,
48+
"project": jira.project(config.project),
49+
}
50+
51+
52+
@main.command()
53+
@click.pass_context
54+
def issues(ctx: click.Context) -> None:
55+
jira = ctx.obj["jira"]
56+
project = ctx.obj["project"]
57+
for issue in search_issues_paginated(
58+
jira,
59+
f"project = {project} AND resolution = Unresolved ORDER BY priority DESC, updated DESC",
60+
):
61+
print(issue, issue.fields.summary)
62+
63+
64+
@main.command()
65+
@click.pass_context
66+
def blocker(ctx: click.Context) -> None:
67+
jira = ctx.obj["jira"]
68+
project = ctx.obj["project"]
69+
for issue in jira.search_issues(
70+
f"project = {project} AND resolution = Unresolved AND priority = blocker ORDER BY updated DESC"
71+
):
72+
print(issue, issue.fields.summary)
73+
74+
75+
@main.command()
76+
@click.pass_context
77+
def my_issues(ctx: click.Context) -> None:
78+
jira = ctx.obj["jira"]
79+
for issue in jira.search_issues(
80+
"assignee = currentUser() AND resolution = Unresolved order by updated DESC"
81+
):
82+
print(issue, issue.fields.summary)
83+
84+
85+
@main.command()
86+
def my_next_issue() -> None:
87+
print("Who do you think I am?")
88+
89+
90+
@main.command()
91+
@click.argument("search_phrase")
92+
@click.pass_context
93+
def search(
94+
ctx: click.Context,
95+
search_phrase: str,
96+
) -> None:
97+
jira = ctx.obj["jira"]
98+
project = ctx.obj["project"]
99+
jql = f"project = {project} AND resolution = Unresolved AND text ~ '{search_phrase}' ORDER BY updated DESC"
100+
for issue in jira.search_issues(jql):
101+
print(issue, issue.fields.summary)
102+
103+
104+
@main.command()
105+
@click.option("--raw", is_flag=True, default=False)
106+
@click.argument("issue_id")
107+
@click.pass_context
108+
def show(
109+
ctx: click.Context,
110+
raw: bool,
111+
issue_id: str,
112+
) -> None:
113+
jira = ctx.obj["jira"]
114+
issue = jira.issue(issue_id)
115+
if raw:
116+
raw_issue = issue.raw
117+
raw_issue["fields"] = remove_empty_attributes(raw_issue["fields"])
118+
raw_issue = remove_empty_attributes(raw_issue)
119+
print(json.dumps(raw_issue))
120+
else:
121+
print(issue)
122+
print(issue.fields.summary)
123+
print(issue.fields.description)
124+
print("Status:", issue.fields.status.name)
125+
fields = jira.fields()
126+
for fieldname in ["Story Points", "Resolution"]:
127+
field_id = next(
128+
(field["id"] for field in fields if field["name"] == fieldname)
129+
)
130+
print(fieldname + ":", issue.get_field(field_id))
131+
132+
133+
@main.command()
134+
@click.option("--assign/--no-assign", default=True)
135+
@click.argument("summary")
136+
@click.argument("description")
137+
@click.pass_context
138+
def create(
139+
ctx: click.Context,
140+
assign: bool,
141+
summary: str,
142+
description: str,
143+
) -> None:
144+
jira = ctx.obj["jira"]
145+
fields: t.MutableMapping[str, t.Any] = {
146+
"project": str(ctx.obj["project"]),
147+
"issuetype": "Task",
148+
"summary": summary,
149+
"description": description,
150+
}
151+
if assign:
152+
fields["assignee"] = {"name": jira.current_user()}
153+
issue = jira.create_issue(fields)
154+
print(issue)
155+
156+
157+
@main.command()
158+
@click.argument("issue_id")
159+
@click.argument("story_points")
160+
@click.pass_context
161+
def storypoint(
162+
ctx: click.Context,
163+
issue_id: str,
164+
story_points: float,
165+
):
166+
"""
167+
Mark issue with a certain number of storypoints.
168+
"""
169+
jira = ctx.obj["jira"]
170+
issue = jira.issue(issue_id)
171+
sp_field_id = next(
172+
(field["id"] for field in jira.fields() if field["name"] == "Story Points")
173+
)
174+
print("Story Points:", issue.get_field(sp_field_id), "->", story_points)
175+
issue.update(fields={sp_field_id: float(story_points)})
176+
177+
178+
@main.command()
179+
@click.argument("issue_id")
180+
@click.pass_context
181+
def close(
182+
ctx: click.Context,
183+
issue_id: str,
184+
):
185+
"""
186+
Close issue as done.
187+
(ATM this is the only supported resolution.)
188+
"""
189+
jira = ctx.obj["jira"]
190+
issue = jira.issue(issue_id)
191+
resolution_id = next((res.id for res in jira.resolutions() if res.name == "Done"))
192+
transitions = jira.transitions(issue)
193+
close_id = next((t["id"] for t in transitions if t["name"] == "Closed"))
194+
jira.transition_issue(issue, close_id, resolution={"id": resolution_id})
195+
196+
197+
@main.command()
198+
@click.pass_context
199+
def types(ctx: click.Context) -> None:
200+
"""
201+
Dump available issue types for the project.
202+
"""
203+
jira = ctx.obj["jira"]
204+
for issue_type in jira.issue_types_for_project(ctx.obj["project"]):
205+
print(issue_type.name, f"(id={issue_type.id})")
206+
207+
208+
@main.command()
209+
@click.pass_context
210+
def resolutions(ctx: click.Context) -> None:
211+
"""
212+
Dump available resolutions.
213+
"""
214+
jira = ctx.obj["jira"]
215+
for resolution in jira.resolutions():
216+
print(resolution.name, f"(id={resolution.id})")
217+
218+
219+
@main.command()
220+
@click.pass_context
221+
def fields(ctx: click.Context) -> None:
222+
"""
223+
Dump available fields.
224+
"""
225+
jira = ctx.obj["jira"]
226+
for field in jira.fields():
227+
print(field["name"], "(id=" + field["id"] + ")")
228+
229+
230+
@main.command()
231+
@click.pass_context
232+
def shell(ctx: click.Context) -> None:
233+
"""
234+
Start an interactive ipython shell.
235+
236+
The objects `jira` and `project` are preloaded.
237+
"""
238+
import IPython
239+
240+
IPython.start_ipython(argv=[], user_ns=ctx.obj)
241+
242+
243+
if __name__ == "__main__":
244+
main()

0 commit comments

Comments
 (0)