Skip to content

Commit ff34298

Browse files
committed
Initial entrypoint + util module implementation
simple lookup and calculation of metric for delay from PR opening to getting first review comment, and PR getting review label to getting first comment
1 parent 56b2859 commit ff34298

File tree

10 files changed

+211
-12
lines changed

10 files changed

+211
-12
lines changed

.flake8

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[flake8]
2+
max-line-length = 100

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
# Ignore dynaconf secret files
3+
.secrets.*
4+
5+
.eggs
6+
.idea
7+
.vscode
8+
.qe-pr-metrics
9+
qe_pr_metrics.egg-info/
10+
11+
settings.yaml

.pre-commit-config.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
repos:
2+
- repo: https://github.com/asottile/reorder_python_imports
3+
rev: v2.3.0
4+
hooks:
5+
- id: reorder-python-imports
6+
- repo: https://github.com/pre-commit/pre-commit-hooks
7+
rev: v3.1.0
8+
hooks:
9+
- id: trailing-whitespace
10+
- id: end-of-file-fixer
11+
- id: check-yaml
12+
- id: debug-statements
13+
- repo: https://github.com/psf/black
14+
rev: 19.10b0
15+
hooks:
16+
- id: black
17+
- repo: https://gitlab.com/pycqa/flake8
18+
rev: 3.8.3
19+
hooks:
20+
- id: flake8

config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from dynaconf import Dynaconf
2+
3+
settings = Dynaconf(
4+
envvar_prefix="METRICS", settings_files=["settings.yaml", ".secrets.yaml"],
5+
)

scripts/gh_metrics.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import json
2+
import time
3+
from pathlib import Path
4+
5+
import click
6+
from tabulate import tabulate
7+
8+
from config import settings
9+
from utils import github_client
10+
11+
12+
# parent click group for gather and graph commands
13+
@click.group()
14+
def generate_metrics():
15+
pass
16+
17+
18+
@generate_metrics.command("gather", help="Gather PR metrics for given GH repo")
19+
@click.option("--repo-name", default="SatelliteQE/robottelo")
20+
@click.option(
21+
"--metric", type=click.Choice(["time_to_comment"]), default="time_to_comment"
22+
)
23+
@click.option(
24+
"--file-output",
25+
default=settings.get("metrics_output_file_prefix", "gh-pr-metrics"),
26+
help="Will only take file name (with or without extension), but not a full path."
27+
"Will append an epoch timestamp to the file name.",
28+
)
29+
def gather(repo_name, metric, output):
30+
metrics = getattr(github_client, metric)(
31+
repo_name=repo_name
32+
) # execute method from github util
33+
34+
click.echo(tabulate(metrics.values(), showindex=metrics.keys(), headers="keys"))
35+
36+
output_filename = f"{Path(output.stem)}-{int(time())}.json"
37+
with open(output_filename, "w") as output_file:
38+
json.dump(metrics, output_file)

settings.yaml.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
gh_repo: SatelliteQE/Robottelo
2+
gh_token: <GH token with read>
3+
metrics_output_file_prefix: "gh-pr-metrics"

setup.cfg

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
[metadata]
2-
name = 'qe-pr-metrics'
2+
name = qe-pr-metrics
33
description = 'tool for collecting metrics on PRs'
4+
long-description = file: README.md
5+
long-description-content-type: text/markdown
46
author = 'Mike Shriver'
5-
author_email = '[email protected]'
7+
author-email = '[email protected]'
68
url='https://gitlab.cee.redhat.com/mshriver/qe-pr-metrics'
79

810
[options]
9-
zip_safe = False
10-
include_pacakge_data = True
1111
packages = find:
1212
entry_points = file:entry_points.txt
1313
setup_requires = setuptools_scm>=3.0.0
14-
install_requires =
15-
PyGithub
14+
install_requires =
15+
attrs
16+
attrdict
17+
cached-property
18+
click
1619
dynaconf>=3.0
20+
PyGithub
21+
tabulate
1722

23+
[options.extras_require]
24+
dev =
25+
pre-commit
26+
ipython
1827

28+
[options.entry_points]
29+
console_scripts =
30+
github-metrics = scripts.gh_metrics:generate_metrics

setup.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
11
from setuptools import setup
22

3-
with open('README.md') as readme:
4-
readme_text = readme_file.read()
5-
6-
setup(
7-
use_scm_version=True,
8-
long_description=readme_text,
3+
setup(use_scm_version=True)

utils/__init__.py

Whitespace-only changes.

utils/github_client.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import attr
2+
from attrdict import AttrDict
3+
from cached_property import cached_property
4+
from github import Github
5+
6+
from config import settings
7+
8+
9+
GH_repo = settings.gh_repo
10+
GH_TOKEN = settings.gh_token
11+
12+
gh_api = Github(GH_TOKEN)
13+
14+
"""
15+
Functions for interacting with github's API, calculating a PR's various timing metrics.
16+
"""
17+
18+
19+
class PullRequestMetrics(AttrDict):
20+
"""Dummy class to provide distinct type around AttrDict"""
21+
22+
pass
23+
24+
25+
@attr.s
26+
class PRWrapper(object):
27+
"""Class for compositing additional properties onto the GH PR instance"""
28+
29+
pr = attr.ib() # the GH api PR object
30+
31+
@cached_property
32+
def first_review(self):
33+
"""When the first review on the PR occurred
34+
Returns None if there are no reviews
35+
"""
36+
reviews_not_by_author = [
37+
review
38+
for review in self.pr.get_reviews()
39+
if review.user.login != self.pr.user.login
40+
]
41+
reviews_not_by_author.sort(key=lambda r: r.submitted_at)
42+
return None if not reviews_not_by_author else reviews_not_by_author[0]
43+
44+
@cached_property
45+
def review_label_added(self):
46+
"""Determine when the review label was added"""
47+
events = [
48+
event
49+
for event in self.pr.get_issue_events()
50+
if (event.label and event.label.name == "review")
51+
and event.event == "labeled"
52+
]
53+
return None if not events else events[0]
54+
55+
@cached_property
56+
def create_to_first_review(self):
57+
"""given a PR, calculate the time from its creation to the first review
58+
59+
If the PR had a 'do not merge' label,
60+
use the time that the label was removed instead of when the PR was created
61+
62+
Args:
63+
pr: a PRWrapper object
64+
"""
65+
# days delta as float between pr created and first review
66+
# TODO factor in DO NOT MERGE label event
67+
if self.first_review is None:
68+
return None
69+
else:
70+
return (self.first_review.submitted_at - self.pr.created_at).total_seconds()
71+
72+
@cached_property
73+
def review_label_to_first_review(self):
74+
"""given a PR,
75+
calculate time from the review label being applied to when it got first review
76+
"""
77+
if self.first_review is None or self.review_label_added is None:
78+
return None
79+
else:
80+
return (
81+
self.first_review.submitted_at - self.review_label_added.created_at
82+
).total_seconds()
83+
84+
85+
def time_to_comment(repo_name):
86+
"""Iterate over the PRs in the repo and calculate times to the first comment
87+
88+
Calculates the time delta per-PR from creation to comment, and from 'review' label to comment
89+
90+
Args:
91+
repo_name: string repository name, including the owner/org (example: SatelliteQE/robottelo)
92+
93+
Returns:
94+
dict, keyed on the PR number, where values are dictionaries containing timing metrics
95+
"""
96+
repo = gh_api.get_repo(repo_name)
97+
prs = repo.get_pulls(state="open", sort="created", base="master")
98+
pr_metrics = dict()
99+
for pr in prs:
100+
pr = PRWrapper(pr)
101+
# TODO: multi-threaded processing of PRs
102+
103+
pr_metrics[pr.pr.number] = PullRequestMetrics(
104+
create_to_review=pr.create_to_first_review,
105+
label_to_review=pr.review_label_to_first_review,
106+
)
107+
108+
return pr_metrics
109+
110+
111+
# for debugging purposes
112+
if __name__ == "__main__":
113+
metrics = time_to_comment("SatelliteQE/robottelo")

0 commit comments

Comments
 (0)