Skip to content

Commit 2502efe

Browse files
committed
clean up and formatting
Signed-off-by: Zack Koppert <[email protected]>
1 parent 2a06eaa commit 2502efe

9 files changed

+302
-177
lines changed

Diff for: .coveragerc

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[run]
2+
omit = test*.py

Diff for: .github/dependabot.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
---
12
# To get started with Dependabot version updates, you'll need to specify which
23
# package ecosystems to update and where the package manifests are located.
34
# Please see the documentation for all configuration options:

Diff for: .github/release-drafter.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
---
12
name-template: 'v$RESOLVED_VERSION'
23
tag-template: 'v$RESOLVED_VERSION'
34
template: |

Diff for: .github/workflows/release-drafter.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
---
12
name: Release Drafter
23

34
on:

Diff for: .gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Output files
2+
issue_metrics.md
23

34
# Byte-compiled / optimized / DLL files
45
__pycache__/
@@ -138,3 +139,6 @@ dmypy.json
138139

139140
# Cython debug symbols
140141
cython_debug/
142+
143+
# Mac
144+
.DS_Store

Diff for: Makefile

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1+
.PHONY: test
12
test:
2-
pytest -v --cov=. --cov-fail-under=80
3+
pytest -v --cov=. --cov-config=.coveragerc --cov-fail-under=80 --cov-report term-missing
4+
5+
.PHONY: clean
6+
clean:
7+
rm -rf .pytest_cache .coverage __pycache__

Diff for: README.md

+14-2
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,25 @@ jobs:
5757

5858
### Example stale_repos.md output
5959

60-
TODO
60+
```markdown
61+
# Issue Metrics
62+
63+
Average time to first response: 2 days, 3:30:00
64+
Number of issues: 2
65+
66+
| Title | URL | TTFR |
67+
| --- | --- | ---: |
68+
| Issue 2 | https://github.com/user/repo/issues/2 | 3 days, 4:30:00 |
69+
| Issue 1 | https://github.com/user/repo/issues/1 | 1 day, 2:30:00 |
70+
71+
```
6172

6273
## Local usage without Docker
6374

6475
1. Copy `.env-example` to `.env`
6576
1. Fill out the `.env` file with a _token_ from a user that has access to the organization to scan (listed below). Tokens should have admin:org or read:org access.
66-
TODO: Make sure this is accurate
77+
1. Fill out the `.env` file with the _repository_url_ of the repository to scan
78+
1. Fill out the `.env` file with the _search_query_ to filter issues by
6779
1. `pip install -r requirements.txt`
6880
1. Run `python3 ./issue_metrics.py`, which will output issue metrics data
6981

Diff for: issue_metrics.py

+65-39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
1+
"""A script for measuring time to first response for GitHub issues.
2+
3+
This script uses the GitHub API to search for issues in a repository and measure
4+
the time to first response for each issue. It then calculates the average time
5+
to first response and writes the issues with their time to first response to a
6+
markdown file.
7+
8+
Functions:
9+
search_issues: Search for issues in a GitHub repository.
10+
auth_to_github: Authenticate to the GitHub API.
11+
measure_time_to_first_response: Measure the time to first response for a GitHub issue.
12+
get_average_time_to_first_response: Calculate the average time to first response for
13+
a list of issues.
14+
write_to_markdown: Write the issues with metrics to a markdown file.
15+
16+
"""
17+
118
import os
2-
from datetime import datetime
19+
from datetime import datetime, timedelta
320
from os.path import dirname, join
421
from urllib.parse import urlparse
522

@@ -13,24 +30,25 @@ def search_issues(repository_url, issue_search_query, github_connection):
1330
1431
Args:
1532
repository_url (str): The URL of the repository to search in.
33+
ie https://github.com/user/repo
1634
issue_search_query (str): The search query to use for finding issues.
1735
github_connection (github3.GitHub): A connection to the GitHub API.
1836
1937
Returns:
2038
List[github3.issues.Issue]: A list of issues that match the search query.
2139
"""
40+
print("Searching for issues...")
2241
# Parse the repository owner and name from the URL
2342
parsed_url = urlparse(repository_url)
2443
path = parsed_url.path.strip("/")
25-
44+
print(f"parsing URL: {repository_url}")
2645
# Split the path into owner and repo
2746
owner, repo = path.split("/")
28-
29-
# Get the repository object
30-
repo = github_connection.repository(owner, repo) # type: ignore
47+
print(f"owner: {owner}, repo: {repo}")
3148

3249
# Search for issues that match the query
33-
issues = repo.search_issues(issue_search_query) # type: ignore
50+
full_query = f"repo:{owner}/{repo} {issue_search_query}"
51+
issues = github_connection.search_issues(full_query) # type: ignore
3452

3553
# Print the issue titles
3654
for issue in issues:
@@ -59,8 +77,8 @@ def measure_time_to_first_response(issues):
5977
issues (list of github3.Issue): A list of GitHub issues.
6078
6179
Returns:
62-
list of github3.Issue: A list of GitHub issues with the time to first response
63-
added as an attribute.
80+
list of tuple: A list of tuples containing a GitHub issue
81+
title, url, and its time to first response.
6482
6583
Raises:
6684
TypeError: If the input is not a list of GitHub issues.
@@ -69,22 +87,31 @@ def measure_time_to_first_response(issues):
6987
issues_with_metrics = []
7088
for issue in issues:
7189
# Get the first comment
72-
first_comment = issue.comments()[0] # type: ignore
73-
74-
# Get the created_at time for the first comment
75-
first_comment_time = datetime.fromisoformat(first_comment.created_at) # type: ignore
76-
77-
# Get the created_at time for the issue
78-
issue_time = datetime.fromisoformat(issue.created_at) # type: ignore
79-
80-
# Calculate the time between the issue and the first comment
81-
time_to_first_response = first_comment_time - issue_time
82-
83-
# Add the time to the issue
84-
issue.time_to_first_response = time_to_first_response
90+
if issue.comments <= 0:
91+
first_comment_time = None
92+
time_to_first_response = None
93+
else:
94+
comments = issue.issue.comments(
95+
number=1, sort="created", direction="asc"
96+
) # type: ignore
97+
for comment in comments:
98+
# Get the created_at time for the first comment
99+
first_comment_time = comment.created_at # type: ignore
100+
101+
# Get the created_at time for the issue
102+
issue_time = datetime.fromisoformat(issue.created_at) # type: ignore
103+
104+
# Calculate the time between the issue and the first comment
105+
time_to_first_response = first_comment_time - issue_time # type: ignore
85106

86107
# Add the issue to the list of issues with metrics
87-
issues_with_metrics.append(issue)
108+
issues_with_metrics.append(
109+
[
110+
issue.title,
111+
issue.html_url,
112+
time_to_first_response,
113+
]
114+
)
88115

89116
return issues_with_metrics
90117

@@ -97,21 +124,26 @@ def get_average_time_to_first_response(issues):
97124
first response added as an attribute.
98125
99126
Returns:
100-
datetime.timedelta: The average time to first response for the issues.
127+
datetime.timedelta: The average time to first response for the issues in seconds.
101128
102129
Raises:
103130
TypeError: If the input is not a list of GitHub issues.
104131
105132
"""
106133
total_time_to_first_response = 0
107134
for issue in issues:
108-
total_time_to_first_response += issue.time_to_first_response.total_seconds()
135+
total_time_to_first_response += issue[2].total_seconds()
109136

110-
average_time_to_first_response = total_time_to_first_response / len(
137+
average_seconds_to_first_response = total_time_to_first_response / len(
111138
issues
112139
) # type: ignore
113140

114-
return average_time_to_first_response
141+
# Print the average time to first response converting seconds to a readable time format
142+
print(
143+
f"Average time to first response: {timedelta(seconds=average_seconds_to_first_response)}"
144+
)
145+
146+
return timedelta(seconds=average_seconds_to_first_response)
115147

116148

117149
def write_to_markdown(issues_with_metrics, average_time_to_first_response, file=None):
@@ -136,10 +168,10 @@ def write_to_markdown(issues_with_metrics, average_time_to_first_response, file=
136168
f"Average time to first response: {average_time_to_first_response}\n"
137169
)
138170
file.write(f"Number of issues: {len(issues_with_metrics)}\n\n")
139-
file.write("| Issue | TTFR |\n")
140-
file.write("| --- | ---: |\n")
141-
for issue, ttfr in issues_with_metrics:
142-
file.write(f"| {issue} | {ttfr} |\n")
171+
file.write("| Title | URL | TTFR |\n")
172+
file.write("| --- | --- | ---: |\n")
173+
for title, url, ttfr in issues_with_metrics:
174+
file.write(f"| {title} | {url} | {ttfr} |\n")
143175
print("Wrote issue metrics to issue_metrics.md")
144176

145177

@@ -170,25 +202,19 @@ def main():
170202
if not issue_search_query:
171203
raise ValueError("ISSUE_SEARCH_QUERY environment variable not set")
172204

173-
issue_search_query = os.getenv("REPOSITORY_URL")
174-
if not issue_search_query:
205+
repo_url = os.getenv("REPOSITORY_URL")
206+
if not repo_url:
175207
raise ValueError("REPOSITORY_URL environment variable not set")
176208

177209
# Search for issues
178-
issues = search_issues(issue_search_query, issue_search_query, github_connection)
179-
180-
# Print the number of issues found
181-
print(f"Found {len(issues)} issues")
210+
issues = search_issues(repo_url, issue_search_query, github_connection)
182211

183212
# Find the time to first response
184213
issues_with_ttfr = measure_time_to_first_response(issues)
185214
average_time_to_first_response = get_average_time_to_first_response(
186215
issues_with_ttfr
187216
)
188217

189-
# Print the average time to first response
190-
print(f"Average time to first response: {average_time_to_first_response}")
191-
192218
# Write the results to a markdown file
193219
write_to_markdown(issues_with_ttfr, average_time_to_first_response)
194220

0 commit comments

Comments
 (0)