Skip to content

Commit

Permalink
feat: add get_stats_time_in_draft in order to calculate the overall d…
Browse files Browse the repository at this point in the history
…raft time stats for multiple issues

Signed-off-by: Zack Koppert <[email protected]>
  • Loading branch information
zkoppert committed Oct 23, 2024
1 parent fe2bc4a commit 4b6b4bc
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 39 deletions.
24 changes: 13 additions & 11 deletions issue_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from markdown_writer import write_to_markdown
from most_active_mentors import count_comments_per_user, get_mentor_count
from search import get_owners_and_repositories, search_issues
from time_in_draft import measure_time_in_draft
from time_in_draft import get_stats_time_in_draft, measure_time_in_draft
from time_to_answer import get_stats_time_to_answer, measure_time_to_answer
from time_to_close import get_stats_time_to_close, measure_time_to_close
from time_to_first_response import (
Expand Down Expand Up @@ -299,6 +299,7 @@ def main(): # pragma: no cover
stats_time_to_close = get_stats_time_to_close(issues_with_metrics)

stats_time_to_answer = get_stats_time_to_answer(issues_with_metrics)
stats_time_in_draft = get_stats_time_in_draft(issues_with_metrics)

num_mentor_count = 0
if enable_mentor_count:
Expand All @@ -310,16 +311,17 @@ def main(): # pragma: no cover

# Write the results to json and a markdown file
write_to_json(
issues_with_metrics,
stats_time_to_first_response,
stats_time_to_close,
stats_time_to_answer,
stats_time_in_labels,
num_issues_open,
num_issues_closed,
num_mentor_count,
search_query,
output_file,
issues_with_metrics=issues_with_metrics,
stats_time_to_first_response=stats_time_to_first_response,
stats_time_to_close=stats_time_to_close,
stats_time_to_answer=stats_time_to_answer,
stats_time_in_draft=stats_time_in_draft,
stats_time_in_labels=stats_time_in_labels,
num_issues_opened=num_issues_open,
num_issues_closed=num_issues_closed,
num_mentor_count=num_mentor_count,
search_query=search_query,
output_file=output_file,
)

write_to_markdown(
Expand Down
77 changes: 52 additions & 25 deletions json_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
Functions:
write_to_json(
issues_with_metrics: List[IssueWithMetrics],
average_time_to_first_response: timedelta,
average_time_to_close: timedelta,
average_time_to_answer: timedelta,
num_issues_opened: int,
num_issues_closed: int,
issues_with_metrics: Union[List[IssueWithMetrics], None],
stats_time_to_first_response: Union[dict[str, timedelta], None],
stats_time_to_close: Union[dict[str, timedelta], None],
stats_time_to_answer: Union[dict[str, timedelta], None],
stats_time_in_draft: Union[dict[str, timedelta], None],
stats_time_in_labels: Union[dict[str, dict[str, timedelta]], None],
num_issues_opened: Union[int, None],
num_issues_closed: Union[int, None],
num_mentor_count: Union[int, None],
search_query: str,
output_file: str,
) -> str:
Expand All @@ -28,6 +31,7 @@ def write_to_json(
stats_time_to_first_response: Union[dict[str, timedelta], None],
stats_time_to_close: Union[dict[str, timedelta], None],
stats_time_to_answer: Union[dict[str, timedelta], None],
stats_time_in_draft: Union[dict[str, timedelta], None],
stats_time_in_labels: Union[dict[str, dict[str, timedelta]], None],
num_issues_opened: Union[int, None],
num_issues_closed: Union[int, None],
Expand All @@ -40,38 +44,48 @@ def write_to_json(
json structure is like following
{
"average_time_to_first_response": "2 days, 12:00:00",
"average_time_to_close": "5 days, 0:00:00",
"average_time_to_answer": "1 day, 0:00:00",
"average_time_to_first_response": "None",
"average_time_to_close": "None",
"average_time_to_answer": "None",
"average_time_in_draft": "None",
"average_time_in_labels": {},
"median_time_to_first_response": "None",
"median_time_to_close": "None",
"median_time_to_answer": "None",
"median_time_in_draft": "None",
"median_time_in_labels": {},
"90_percentile_time_to_first_response": "None",
"90_percentile_time_to_close": "None",
"90_percentile_time_to_answer": "None",
"90_percentile_time_in_draft": "None",
"90_percentile_time_in_labels": {},
"num_items_opened": 2,
"num_items_closed": 1,
"num_items_closed": 0,
"num_mentor_count": 5,
"total_item_count": 2,
"issues": [
{
"title": "Issue 1",
"html_url": "https://github.com/owner/repo/issues/1",
"author": "author",
"time_to_first_response": "3 days, 0:00:00",
"time_to_close": "6 days, 0:00:00",
"author": "alice",
"time_to_first_response": "None",
"time_to_close": "None",
"time_to_answer": "None",
"time_in_draft": "1 day, 0:00:00",
"label_metrics": {
"bug": "1 day, 16:24:12"
}
"time_in_draft": "None",
"label_metrics": {}
},
{
"title": "Issue 2",
"html_url": "https://github.com/owner/repo/issues/2",
"author": "author",
"time_to_first_response": "2 days, 0:00:00",
"time_to_close": "4 days, 0:00:00",
"time_to_answer": "1 day, 0:00:00",
"label_metrics": {
}
},
"author": "bob",
"time_to_first_response": "None",
"time_to_close": "None",
"time_to_answer": "None",
"time_in_draft": "None",
"label_metrics": {}
}
],
"search_query": "is:issue is:open repo:owner/repo"
"search_query": "is:issue repo:owner/repo"
}
"""
Expand Down Expand Up @@ -107,6 +121,16 @@ def write_to_json(
med_time_to_answer = stats_time_to_answer["med"]
p90_time_to_answer = stats_time_to_answer["90p"]

# time in draft
average_time_in_draft = None
med_time_in_draft = None
p90_time_in_draft = None
if stats_time_in_draft is not None:
average_time_in_draft = stats_time_in_draft["avg"]
med_time_in_draft = stats_time_in_draft["med"]
p90_time_in_draft = stats_time_in_draft["90p"]

# time in labels
average_time_in_labels = {}
med_time_in_labels = {}
p90_time_in_labels = {}
Expand All @@ -123,14 +147,17 @@ def write_to_json(
"average_time_to_first_response": str(average_time_to_first_response),
"average_time_to_close": str(average_time_to_close),
"average_time_to_answer": str(average_time_to_answer),
"average_time_in_draft": str(average_time_in_draft),
"average_time_in_labels": average_time_in_labels,
"median_time_to_first_response": str(med_time_to_first_response),
"median_time_to_close": str(med_time_to_close),
"median_time_to_answer": str(med_time_to_answer),
"median_time_in_draft": str(med_time_in_draft),
"median_time_in_labels": med_time_in_labels,
"90_percentile_time_to_first_response": str(p90_time_to_first_response),
"90_percentile_time_to_close": str(p90_time_to_close),
"90_percentile_time_to_answer": str(p90_time_to_answer),
"90_percentile_time_in_draft": str(p90_time_in_draft),
"90_percentile_time_in_labels": p90_time_in_labels,
"num_items_opened": num_issues_opened,
"num_items_closed": num_issues_closed,
Expand Down
14 changes: 14 additions & 0 deletions test_json_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ def test_write_to_json(self):
"med": timedelta(days=2),
"90p": timedelta(days=3),
}
stats_time_in_draft = {
"avg": timedelta(days=1),
"med": timedelta(days=1),
"90p": timedelta(days=1),
}
stats_time_in_labels = {
"avg": {"bug": timedelta(days=1, hours=16, minutes=24, seconds=12)},
"med": {"bug": timedelta(days=1, hours=16, minutes=24, seconds=12)},
Expand All @@ -68,14 +73,17 @@ def test_write_to_json(self):
"average_time_to_first_response": "2 days, 12:00:00",
"average_time_to_close": "5 days, 0:00:00",
"average_time_to_answer": "1 day, 0:00:00",
"average_time_in_draft": "1 day, 0:00:00",
"average_time_in_labels": {"bug": "1 day, 16:24:12"},
"median_time_to_first_response": "2 days, 12:00:00",
"median_time_to_close": "4 days, 0:00:00",
"median_time_to_answer": "2 days, 0:00:00",
"median_time_in_draft": "1 day, 0:00:00",
"median_time_in_labels": {"bug": "1 day, 16:24:12"},
"90_percentile_time_to_first_response": "1 day, 12:00:00",
"90_percentile_time_to_close": "3 days, 0:00:00",
"90_percentile_time_to_answer": "3 days, 0:00:00",
"90_percentile_time_in_draft": "1 day, 0:00:00",
"90_percentile_time_in_labels": {"bug": "1 day, 16:24:12"},
"num_items_opened": 2,
"num_items_closed": 1,
Expand Down Expand Up @@ -113,6 +121,7 @@ def test_write_to_json(self):
stats_time_to_first_response=stats_time_to_first_response,
stats_time_to_close=stats_time_to_close,
stats_time_to_answer=stats_time_to_answer,
stats_time_in_draft=stats_time_in_draft,
stats_time_in_labels=stats_time_in_labels,
num_issues_opened=num_issues_opened,
num_issues_closed=num_issues_closed,
Expand Down Expand Up @@ -154,6 +163,7 @@ def test_write_to_json_with_no_response(self):
"med": {},
"90p": {},
}
stats_time_in_draft = None
num_issues_opened = 2
num_issues_closed = 0
num_mentor_count = 5
Expand All @@ -162,14 +172,17 @@ def test_write_to_json_with_no_response(self):
"average_time_to_first_response": "None",
"average_time_to_close": "None",
"average_time_to_answer": "None",
"average_time_in_draft": "None",
"average_time_in_labels": {},
"median_time_to_first_response": "None",
"median_time_to_close": "None",
"median_time_to_answer": "None",
"median_time_in_draft": "None",
"median_time_in_labels": {},
"90_percentile_time_to_first_response": "None",
"90_percentile_time_to_close": "None",
"90_percentile_time_to_answer": "None",
"90_percentile_time_in_draft": "None",
"90_percentile_time_in_labels": {},
"num_items_opened": 2,
"num_items_closed": 0,
Expand Down Expand Up @@ -207,6 +220,7 @@ def test_write_to_json_with_no_response(self):
stats_time_to_first_response=stats_time_to_first_response,
stats_time_to_close=stats_time_to_close,
stats_time_to_answer=stats_time_to_answer,
stats_time_in_draft=stats_time_in_draft,
stats_time_in_labels=stats_time_in_labels,
num_issues_opened=num_issues_opened,
num_issues_closed=num_issues_closed,
Expand Down
54 changes: 53 additions & 1 deletion test_time_in_draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from unittest.mock import MagicMock

import pytz
from time_in_draft import measure_time_in_draft
from time_in_draft import get_stats_time_in_draft, measure_time_in_draft


class TestMeasureTimeInDraft(unittest.TestCase):
Expand Down Expand Up @@ -56,5 +56,57 @@ def test_time_in_draft_with_no_events(self):
self.assertIsNone(result, "The result should be None when there are no events.")


class TestGetStatsTimeInDraft(unittest.TestCase):
"""
Unit tests for the get_stats_time_in_draft function.
"""

def test_get_stats_time_in_draft_with_data(self):
"""
Test get_stats_time_in_draft with valid draft times.
"""
issues = [
MagicMock(time_in_draft=timedelta(days=1)),
MagicMock(time_in_draft=timedelta(days=2)),
MagicMock(time_in_draft=timedelta(days=3)),
]

result = get_stats_time_in_draft(issues)
expected = {
"avg": timedelta(days=2),
"med": timedelta(days=2),
"90p": timedelta(days=2, seconds=69120),
}

self.assertEqual(
result, expected, "The statistics for time in draft are incorrect."
)

def test_get_stats_time_in_draft_no_data(self):
"""
Test get_stats_time_in_draft with no draft times.
"""
issues = [
MagicMock(time_in_draft=None),
MagicMock(time_in_draft=None),
]

result = get_stats_time_in_draft(issues)
self.assertIsNone(
result, "The result should be None when there are no draft times."
)

def test_get_stats_time_in_draft_empty_list(self):
"""
Test get_stats_time_in_draft with an empty list of issues.
"""
issues = []

result = get_stats_time_in_draft(issues)
self.assertIsNone(
result, "The result should be None when the list of issues is empty."
)


if __name__ == "__main__":
unittest.main()
46 changes: 44 additions & 2 deletions time_in_draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
This module contains a function that measures the time a pull request has been in draft state.
"""

from datetime import datetime
from typing import Union
from datetime import datetime, timedelta
from typing import List, Union

import github3
import numpy
import pytz
from classes import IssueWithMetrics


def measure_time_in_draft(
Expand Down Expand Up @@ -42,3 +44,43 @@ def measure_time_in_draft(
return None

return None


def get_stats_time_in_draft(
issues_with_metrics: List[IssueWithMetrics],
) -> Union[dict[str, timedelta], None]:
"""
Calculate stats describing the time in draft for a list of issues.
"""
# Filter out issues with no time to answer
issues_with_time_to_draft = [
issue for issue in issues_with_metrics if issue.time_in_draft is not None
]

# Calculate the total time in draft for all issues
draft_times = []
if issues_with_time_to_draft:
for issue in issues_with_time_to_draft:
if issue.time_in_draft:
draft_times.append(issue.time_in_draft.total_seconds())

# Calculate stats describing time to answer
num_issues_with_time_in_draft = len(issues_with_time_to_draft)
if num_issues_with_time_in_draft > 0:
average_time_in_draft = numpy.round(numpy.average(draft_times))
med_time_in_draft = numpy.round(numpy.median(draft_times))
ninety_percentile_time_in_draft = numpy.round(
numpy.percentile(draft_times, 90, axis=0)
)
else:
return None

stats = {
"avg": timedelta(seconds=average_time_in_draft),
"med": timedelta(seconds=med_time_in_draft),
"90p": timedelta(seconds=ninety_percentile_time_in_draft),
}

# Print the average time to answer converting seconds to a readable time format
print(f"Average time to answer: {timedelta(seconds=average_time_in_draft)}")
return stats

0 comments on commit 4b6b4bc

Please sign in to comment.