Skip to content

Commit 2890b7b

Browse files
Add script to provide team velocity
by calculating the weekly numbers of closed issues. Signed-off-by: Frantisek Lachman <[email protected]>
1 parent a166a79 commit 2890b7b

File tree

1 file changed

+227
-0
lines changed

1 file changed

+227
-0
lines changed

get_velocity.py

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright Contributors to the Packit project.
4+
# SPDX-License-Identifier: MIT
5+
6+
from collections import OrderedDict
7+
from datetime import timedelta, datetime
8+
from time import sleep
9+
from typing import List, Tuple, Optional
10+
11+
import click
12+
from github import GithubException
13+
from ogr import GithubService
14+
from ogr.abstract import IssueStatus
15+
from ogr.services.github import GithubProject, GithubIssue
16+
17+
18+
def get_closed_issues(
19+
project: GithubProject, tries=10, interval=10
20+
) -> List[GithubIssue]:
21+
last_ex: Optional[GithubException] = None
22+
for _ in range(tries):
23+
try:
24+
return project.get_issue_list(status=IssueStatus.closed) # type: ignore
25+
except GithubException as ex:
26+
last_ex = ex
27+
click.echo("Retrying because of the API limit.")
28+
sleep(interval)
29+
else:
30+
raise last_ex # type: ignore
31+
32+
33+
def is_counted(issue: GithubIssue) -> bool:
34+
if issue._raw_issue.state_reason == "not_planned":
35+
return False
36+
if not issue.assignees:
37+
return False
38+
return True
39+
40+
41+
def get_week_representation(date_time) -> str:
42+
year = date_time.year
43+
week = date_time.isocalendar()[1]
44+
week_str = f"{year}-{week:02d}"
45+
return week_str
46+
47+
48+
def get_issue_value(
49+
issue: GithubIssue,
50+
labels: Optional[List[Tuple[str, int]]] = None,
51+
value_without_label=0,
52+
):
53+
if not labels:
54+
return 1
55+
56+
issue_labels = [label.name for label in issue.labels]
57+
value: Optional[int] = None
58+
for label_name, label_value in labels:
59+
if label_name in issue_labels:
60+
value = label_value
61+
62+
if value is not None:
63+
return value
64+
65+
click.echo(f"label not set: {issue.url}")
66+
return value_without_label
67+
68+
69+
def get_closed_issues_per_week(
70+
projects,
71+
service,
72+
interval=0,
73+
labels: Optional[List[Tuple[str, int]]] = None,
74+
value_without_label=0,
75+
since: Optional[datetime] = None,
76+
):
77+
since = since or (datetime.now() - timedelta(days=365 * 3))
78+
week_numbers = {}
79+
for week in get_week_numbers_since(since):
80+
week_numbers[week] = 0
81+
click.echo(week_numbers)
82+
for project in projects:
83+
namespace, project_name = project.split("/")
84+
click.echo(f"{namespace}/{project_name}")
85+
gh_project = service.get_project(namespace=namespace, repo=project_name)
86+
for issue in get_closed_issues(gh_project, interval=interval):
87+
if not is_counted(issue):
88+
continue
89+
90+
week_str = get_week_representation(issue._raw_issue.closed_at)
91+
value = get_issue_value(
92+
issue, labels, value_without_label=value_without_label
93+
)
94+
95+
if week_str in week_numbers:
96+
week_numbers[week_str] += value
97+
98+
result = OrderedDict(sorted(week_numbers.items()))
99+
for week, count in result.items():
100+
click.echo(f"{week};{count}")
101+
return week_numbers
102+
103+
104+
def get_week_numbers_since(since: datetime) -> List[str]:
105+
delta = timedelta(days=7)
106+
107+
current_date = datetime.now()
108+
week_numbers = []
109+
while current_date > since:
110+
week_numbers.append(get_week_representation(current_date))
111+
current_date -= delta
112+
return week_numbers
113+
114+
115+
DEFAULT_PROJECTS = [
116+
"packit/ogr",
117+
"packit/requre",
118+
"packit/specfile",
119+
"packit/packit",
120+
"packit/research",
121+
"packit/weekly-roles",
122+
"packit/tokman",
123+
"packit/wait-for-copr",
124+
"packit/sandcastle",
125+
"packit/packit-service-zuul",
126+
"packit/packit-service-fedmsg",
127+
"packit/packit-service",
128+
"packit/dist-git-to-source-git",
129+
"packit/deployment",
130+
"packit/hardly",
131+
"packit/dashboard",
132+
"packit/packit.dev",
133+
"packit/private",
134+
"packit/packit-service-centosmsg",
135+
]
136+
137+
138+
@click.command(
139+
help="Get the weekly number of issues closed",
140+
)
141+
@click.option(
142+
"-t",
143+
"--token",
144+
type=click.STRING,
145+
help="GitHub token",
146+
)
147+
@click.option(
148+
"-s",
149+
"--since",
150+
type=click.DateTime(),
151+
help="Datetime to start the statistic.",
152+
)
153+
@click.option(
154+
"-l",
155+
"--label",
156+
nargs=2,
157+
type=click.Tuple([str, int]),
158+
multiple=True,
159+
help="Set values to labeled issues.\n"
160+
"Later definition has priority.\n"
161+
"Other issues not counted if set.\n"
162+
"All returns 1 when not set.\n",
163+
)
164+
@click.option(
165+
"--value-without-label",
166+
type=click.INT,
167+
default=0,
168+
help="When label option is used,\n"
169+
"this value is used for issues without particular label.",
170+
)
171+
@click.argument(
172+
"project",
173+
type=click.STRING,
174+
nargs=-1,
175+
)
176+
def velocity(token, label, value_without_label, project):
177+
"""
178+
Use NAMESPACE/PROJECT as arguments to get issues from.
179+
180+
E.g. get_velocity --value-without-label 3 \
181+
-t ghp_* -l complexity/easy-fix 2 \
182+
-l complexity/epic 0 \
183+
-l complexity/single-task 5 \
184+
packit/packit packit/ogr
185+
186+
187+
Multiple can be provided.
188+
189+
Default projects:
190+
packit/ogr
191+
packit/requre
192+
packit/specfile
193+
packit/packit
194+
packit/research
195+
packit/weekly-roles
196+
packit/tokman
197+
packit/wait-for-copr
198+
packit/sandcastle
199+
packit/packit-service-zuul
200+
packit/packit-service-fedmsg
201+
packit/packit-service
202+
packit/dist-git-to-source-git
203+
packit/deployment
204+
packit/hardly
205+
packit/dashboard
206+
packit/packit.dev
207+
packit/private
208+
packit/packit-service-centosmsg
209+
"""
210+
project = project or DEFAULT_PROJECTS
211+
212+
click.echo("Projects:\n" + "\n".join(f"* {p}" for p in project))
213+
if label:
214+
click.echo(
215+
"Labels:\n" + "\n".join(f"* {label}: {weight}" for label, weight in label)
216+
)
217+
218+
get_closed_issues_per_week(
219+
projects=project,
220+
service=GithubService(token=token),
221+
labels=label,
222+
value_without_label=value_without_label,
223+
)
224+
225+
226+
if __name__ == "__main__":
227+
velocity(auto_envvar_prefix="TEAM_VELOCITY")

0 commit comments

Comments
 (0)