Repository: chaoss/augur

Issue:
API Endpoint Request
#### Calculate Contributions and Display Developers Contributing n % of all Commits (Repo and Repo Group) 
We want to build a pie chart that shows the commit share of the developers "inside the bus factor", and then provides a slice, as well, for "all other committers". 
1. To orient ourselves, first, how many total commits are there? 
```sql
    select sum(patches) from (select repo_group_id, email, year, patches from dm_repo_group_annual
    where year = 2017
    order by patches desc)a; 
```
For our example, there are 9,671 patches in a repository group of 112 repositories.

2.  What is the distribution of committers and total commits? 
```sql
    SELECT
        repo_group_id,
        email,
        SUM ( patches ) as commitsyear 
    FROM
        ( SELECT repo_group_id, email, YEAR, patches FROM dm_repo_group_annual WHERE YEAR = 2017 ORDER BY patches DESC )  a
    GROUP BY
        repo_group_id,
        email
        order by commitsyear desc; 
```
There are 783 committers in our sample. The top committer has 650 commits, the bottom committer has 1 commit, and there are 411 such individuals. 

3. How many committers are in the set that contributes 50% of the 9,671 commits. 
    - We know the total commits are 9,671, so we need to loop through committers in an ordered list, from the top down, until we arrive at the list of top contributors who contribute 50% of the total commits. In this case,  `1/2*9,671 = 4,835.5`, and we round up to 4,836. 
    - When we loop through top committers, keeping a cumulative total, we add each developer to the list until we exceed 4,836, at which point we have the list. There are 30 committers in our list (plus one row for everyone not in the "developers with the most contributions whose total commits add up to 50% (or other percent) of the total): 
```
        Developer           Commits in a Year
  
```
4. Our pie chart would then have 30 developers, and a 31st slice for "all other contributors". This is a large case, obviously. Perhaps a percentile that is lower would be appropriate. This is parameterizable. 
**Pseudo Code: 
```python
    commits =1; 
    for i in all_committers: 
        commits = commits+developerCommits
        developerlist = add developer to list; 
        if commits > totalcommits/2 
            break; 
```
The end result we want is a list with the commits per developer until we reach 1/2 or some other share of the total commits PLUS, one slice for ALL the other commits made by people outside the group whose commit total, counted from the most commits down, adds up to that percentage (50% or otherwise). 

5. The JSON we would want to return probably looks like: 
```json
    [
        {
            "user_id": 1,
            "email": "s@goggins.com",
            "commits": 333,
            "repo_group_name": "rails",
            "repo_group_id": 21000
        },
        {
            "user_id": 2,
            "email": "d@goggins.com",
            "commits": 331,
            "repo_group_name": "rails",
            "repo_group_id": 21000
        },
        {
            "user_id": 1,
            "email": "other_contributors",
            "commits": 133,
            "repo_group_name": "rails",
            "repo_group_id": 21000
        }
    ]

```


Discussion:
@sgoggins  what do we name this?

Resolving patch:
diff --git a/augur/datasources/augur_db/augur_db.py b/augur/datasources/augur_db/augur_db.py
--- a/augur/datasources/augur_db/augur_db.py
+++ b/augur/datasources/augur_db/augur_db.py
@@ -1212,6 +1212,48 @@ def cii_best_practices_badge(self, repo_group_id, repo_id=None):
             results = pd.read_sql(cii_best_practices_badge_SQL, self.db, params={'repo_id': repo_id})
             return results
 
+    @annotate(tag='average-issue-resolution-time')
+    def average_issue_resolution_time(self, repo_group_id, repo_id=None):
+        """
+        Returns the average issue resolution time
+
+        :param repo_group_id: The repository's repo_group_id
+        :param repo_id: The repository's repo_id, defaults to None
+        :return: Average issue resolution time
+        """
+        if not repo_id:
+            avg_issue_resolution_SQL = s.sql.text("""
+                SELECT
+                    issues.repo_id,
+                    repo.repo_name,
+                    AVG(issues.closed_at - issues.created_at)::text AS avg_issue_resolution_time
+                FROM issues JOIN repo ON issues.repo_id = repo.repo_id
+                WHERE issues.repo_id IN
+                    (SELECT repo_id FROM repo WHERE  repo_group_id = :repo_group_id)
+                AND closed_at IS NOT NULL
+                GROUP BY issues.repo_id, repo.repo_name
+                ORDER BY issues.repo_id
+            """)
+
+            results = pd.read_sql(avg_issue_resolution_SQL, self.db,
+                                  params={'repo_group_id': repo_group_id})
+            return results
+
+        else:
+            avg_issue_resolution_SQL = s.sql.text("""
+                SELECT
+                    repo.repo_name,
+                    AVG(issues.closed_at - issues.created_at)::text AS avg_issue_resolution_time
+                FROM issues JOIN repo ON issues.repo_id = repo.repo_id
+                WHERE issues.repo_id = :repo_id
+                AND closed_at IS NOT NULL
+                GROUP BY repo.repo_name
+            """)
+
+            results = pd.read_sql(avg_issue_resolution_SQL, self.db,
+                                  params={'repo_id': repo_id})
+            return results
+
     @annotate(tag='forks')
     def forks(self, repo_group_id, repo_id=None):
         """
@@ -1938,7 +1980,7 @@ def annual_commit_count_ranked_by_new_repo_in_repo_group(self, repo_group_id, re
         """
         if calendar_year == None:
             calendar_year = 2019
-        
+
         cdRgNewrepRankedCommitsSQL = None
 
         if not repo_id:
@@ -1953,7 +1995,7 @@ def annual_commit_count_ranked_by_new_repo_in_repo_group(self, repo_group_id, re
                 ORDER BY net desc
                 LIMIT 10
             """)
-        else: 
+        else:
             cdRgNewrepRankedCommitsSQL = s.sql.text("""
                 SELECT repo.repo_id, sum(cast(added as INTEGER) - cast(removed as INTEGER) - cast(whitespace as INTEGER)) as net, patches, repo_name
                 FROM dm_repo_annual, repo, repo_groups
@@ -1972,15 +2014,15 @@ def annual_commit_count_ranked_by_new_repo_in_repo_group(self, repo_group_id, re
     @annotate(tag='annual-commit-count-ranked-by-repo-in-repo-group')
     def annual_commit_count_ranked_by_repo_in_repo_group(self, repo_group_id, repo_id=None, timeframe=None):
         """
-        For each repository in a collection of repositories being managed, each REPO's total commits during the current Month, 
-        Year or Week. Result ranked from highest number of commits to lowest by default. 
+        For each repository in a collection of repositories being managed, each REPO's total commits during the current Month,
+        Year or Week. Result ranked from highest number of commits to lowest by default.
         :param repo_group_id: The repository's repo_group_id
         :param repo_id: The repository's repo_id, defaults to None
         :param calendar_year: the calendar year a repo is created in to be considered "new"
-        """    
+        """
         if timeframe == None:
             timeframe = 'all'
-        
+
         cdRgTpRankedCommitsSQL = None
 
         if repo_id:
@@ -2020,7 +2062,7 @@ def annual_commit_count_ranked_by_repo_in_repo_group(self, repo_group_id, repo_i
                     order by net desc
                     LIMIT 10
                 """)
-        else: 
+        else:
             if timeframe == 'all':
                 cdRgTpRankedCommitsSQL = s.sql.text("""
                     SELECT repo.repo_id, repo_name as name, SUM(added - removed - whitespace) as net, patches
@@ -2078,7 +2120,7 @@ def annual_lines_of_code_count_ranked_by_new_repo_in_repo_group(self, repo_group
         """
         if calendar_year == None:
             calendar_year = 2019
-        
+
         cdRgNewrepRankedCommitsSQL = None
 
         if not repo_id:
@@ -2093,7 +2135,7 @@ def annual_lines_of_code_count_ranked_by_new_repo_in_repo_group(self, repo_group
                 ORDER BY net desc
                 LIMIT 10
             """)
-        else: 
+        else:
             cdRgNewrepRankedCommitsSQL = s.sql.text("""
                 SELECT repo.repo_id, sum(cast(added as INTEGER) - cast(removed as INTEGER) - cast(whitespace as INTEGER)) as net, patches, repo_name
                 FROM dm_repo_annual, repo, repo_groups
@@ -2112,15 +2154,15 @@ def annual_lines_of_code_count_ranked_by_new_repo_in_repo_group(self, repo_group
     @annotate(tag='annual-lines-of-code-count-ranked-by-repo-in-repo-group')
     def annual_lines_of_code_count_ranked_by_repo_in_repo_group(self, repo_group_id, repo_id=None, timeframe=None):
         """
-        For each repository in a collection of repositories being managed, each REPO's total commits during the current Month, 
-        Year or Week. Result ranked from highest number of commits to lowest by default. 
+        For each repository in a collection of repositories being managed, each REPO's total commits during the current Month,
+        Year or Week. Result ranked from highest number of commits to lowest by default.
         :param repo_group_id: The repository's repo_group_id
         :param repo_id: The repository's repo_id, defaults to None
         :param calendar_year: the calendar year a repo is created in to be considered "new"
-        """    
+        """
         if timeframe == None:
             timeframe = 'all'
-        
+
         cdRgTpRankedCommitsSQL = None
 
         if repo_id:
@@ -2160,7 +2202,7 @@ def annual_lines_of_code_count_ranked_by_repo_in_repo_group(self, repo_group_id,
                     order by net desc
                     LIMIT 10
                 """)
-        else: 
+        else:
             if timeframe == 'all':
                 cdRgTpRankedCommitsSQL = s.sql.text("""
                     SELECT repo.repo_id, repo_name as name, SUM(added - removed - whitespace) as net, patches
@@ -2205,6 +2247,107 @@ def annual_lines_of_code_count_ranked_by_repo_in_repo_group(self, repo_group_id,
         "repo_id": repo_id})
         return results
 
+    @annotate(tag='top-committers')
+    def top_committers(self, repo_group_id, repo_id=None, year=None, threshold=0.5):
+        """
+        Returns a list of contributors contributing N% of all commits.
+
+        :param repo_group_id: Repo group ID
+        :param repo_id: Repo ID.
+        :param year: Year. eg: 2018, 2107. Defaults to current year.
+        :param threshold: The threshold to specify N%. Defaults to 0.5
+        """
+        threshold = float(threshold)
+        if threshold < 0 or threshold > 1:
+            raise ValueError('threshold should be between 0 and 1')
+
+        if year is None:
+            year = datetime.datetime.now().year
+
+        if not repo_id:
+            total_commits_SQL = s.sql.text("""
+                SELECT SUM(patches)::int
+                FROM
+                    (SELECT repo_group_id, email, year, patches
+                    FROM dm_repo_group_annual
+                    WHERE year = :year AND repo_group_id = :repo_group_id
+                    ORDER BY patches DESC) a
+            """)
+
+            results = pd.read_sql(total_commits_SQL, self.db,
+                                params={'year': year, 'repo_group_id': repo_group_id})
+        else:
+            total_commits_SQL = s.sql.text("""
+                SELECT SUM(patches)::int
+                FROM
+                    (SELECT repo_id, email, year, patches
+                    FROM dm_repo_annual
+                    WHERE year = :year AND repo_id = :repo_id
+                    ORDER BY patches DESC) a
+            """)
+
+            results = pd.read_sql(total_commits_SQL, self.db,
+                                params={'year': year, 'repo_id': repo_id})
+
+        total_commits = int(results.iloc[0]['sum'])
+        threshold_commits = round(threshold * total_commits)
+
+        if not repo_id:
+            committers_SQL = s.sql.text("""
+                SELECT
+                    a.repo_group_id,
+                    rg_name AS repo_group_name,
+                    a.email,
+                    SUM(a.patches)::int AS commits
+                FROM
+                    (SELECT repo_group_id, email, year, patches
+                    FROM dm_repo_group_annual
+                    WHERE year = :year AND repo_group_id = :repo_group_id
+                    ORDER BY patches DESC) a, repo_groups
+                WHERE a.repo_group_id = repo_groups.repo_group_id
+                GROUP BY a.repo_group_id, repo_group_name, a.email
+                ORDER BY commits DESC
+            """)
+
+            results = pd.read_sql(committers_SQL, self.db,
+                                params={'year': year, 'repo_group_id': repo_group_id})
+        else:
+            committers_SQL = s.sql.text("""
+                SELECT
+                    a.repo_id,
+                    repo.repo_name,
+                    a.email,
+                    SUM(a.patches)::int AS commits
+                FROM
+                    (SELECT repo_id, email, year, patches
+                    FROM dm_repo_annual
+                    WHERE year = :year AND repo_id = :repo_id
+                    ORDER BY patches DESC) a, repo
+                WHERE a.repo_id = repo.repo_id
+                GROUP BY a.repo_id, repo.repo_name, a.email
+                ORDER BY commits DESC
+            """)
+
+            results = pd.read_sql(committers_SQL, self.db,
+                                  params={'year': year, 'repo_id': repo_id})
+
+        cumsum = 0
+        for i, row in results.iterrows():
+            cumsum += row['commits']
+            if cumsum >= threshold_commits:
+                results = results[:i + 1]
+                break
+
+        if not repo_id:
+            rg_name = results.iloc[0]['repo_group_name']
+            results.loc[i+1] = [repo_group_id, rg_name, 'other_contributors',
+                                int(total_commits - cumsum)]
+        else:
+            repo_name = results.iloc[0]['repo_name']
+            results.loc[i+1] = [repo_id, repo_name, 'other_contributors',
+                                int(total_commits - cumsum)]
+
+        return results
 
 #####################################
 ###           UTILITIES           ###
diff --git a/augur/datasources/augur_db/routes.py b/augur/datasources/augur_db/routes.py
--- a/augur/datasources/augur_db/routes.py
+++ b/augur/datasources/augur_db/routes.py
@@ -1392,6 +1392,47 @@ def get_repos_for_dosocs():
     """
     server.addRepoMetric(augur_db.cii_best_practices_badge, 'cii-best-practices-badge')
 
+    """
+    @api {get} /repo-groups/:repo_group_id/avgerage-issue-resolution-time Average Issue Resolution Time (Repo Group)
+    @apiName average-issue-resolution-time-repo-group
+    @apiGroup Risk
+    @apiDescription The average issue resolution time.
+                    <a href="https://github.com/chaoss/wg-risk/blob/master/focus-areas/business-risk.md">CHAOSS Metric Definition</a>
+    @apiParam {string} repo_group_id Repository Group ID
+    @apiSuccessExample {json} Success-Response:
+                    [
+                        {
+                            "repo_id": 21353,
+                            "repo_name": "open_id_authentication",
+                            "avg_issue_resolution_time": "1413 days 15:39:48"
+                        },
+                        {
+                            "repo_id": 21362,
+                            "repo_name": "country_select",
+                            "avg_issue_resolution_time": "140 days 09:37:58.2"
+                        }
+                    ]
+    """
+    server.addRepoGroupMetric(augur_db.average_issue_resolution_time, 'average-issue-resolution-time')
+
+    """
+    @api {get} /repo-groups/:repo_group_id/repos/:repo_id/avgerage-issue-resolution-time Average Issue Resolution Time (Repo)
+    @apiName average-issue-resolution-time-repo
+    @apiGroup Risk
+    @apiDescription The average issue resolution time.
+                    <a href="https://github.com/chaoss/wg-risk/blob/master/focus-areas/business-risk.md">CHAOSS Metric Definition</a>
+    @apiParam {string} repo_group_id Repository Group ID.
+    @apiParam {string} repo_id Repository ID.
+    @apiSuccessExample {json} Success-Response:
+                    [
+                        {
+                            "repo_name": "maven-release",
+                            "avg_issue_resolution_time": "276 days 13:54:13.2"
+                        }
+                    ]
+    """
+    server.addRepoMetric(augur_db.average_issue_resolution_time, 'average-issue-resolution-time')
+
     """
     @api {get} /repo-groups/:repo_group_id/forks Forks (Repo Group)
     @apiName forks-repo-group
@@ -2095,4 +2136,77 @@ def get_repos_for_dosocs():
                             }
                         ]
     """
-    server.addRepoMetric(augur_db.annual_lines_of_code_count_ranked_by_repo_in_repo_group,'annual-lines-of-code-count-ranked-by-repo-in-repo-group')
\ No newline at end of file
+    server.addRepoMetric(augur_db.annual_lines_of_code_count_ranked_by_repo_in_repo_group,'annual-lines-of-code-count-ranked-by-repo-in-repo-group')
+
+    """
+    @api {get} /repo-groups/:repo_group_id/top-committers Top Committers (Repo Group)
+    @apiName top-committers-repo-group
+    @apiGroup Experimental
+    @apiDescription Returns a list of contributors contributing N% of all commits.
+    @apiParam {string} repo_group_id Repository Group ID
+    @apiParam {string} [year] Specify the year to return the results for. Default value: `current year`
+    @apiParam {string} [threshold=0.5] Specify N%. Accepts a value between `0` & `1` where `0` specifies
+                                        `0%` and `1` specifies `100%`.
+    @apiSuccessExample {json} Success-Response:
+                    [
+                        {
+                            "repo_group_id": 20,
+                            "repo_group_name": "Rails",
+                            "email": "kamipo@gmail.com",
+                            "commits": 502
+                        },
+                        {
+                            "repo_group_id": 20,
+                            "repo_group_name": "Rails",
+                            "email": "rafaelmfranca@gmail.com",
+                            "commits": 246
+                        },
+                        {
+                            "repo_group_id": 20,
+                            "repo_group_name": "Rails",
+                            "email": "kaspth@gmail.com",
+                            "commits": 119
+                        },
+                        {
+                            "repo_group_id": "20",
+                            "repo_group_name": "Rails",
+                            "email": "other_contributors",
+                            "commits": 1774
+                        }
+                    ]
+    """
+    server.addRepoGroupMetric(augur_db.top_committers, 'top-committers')
+
+    """
+    @api {get} /repo-groups/:repo_group_id/repos/:repo_id/top-committers Top Committers (Repo)
+    @apiName top-committers-repo
+    @apiGroup Experimental
+    @apiDescription Returns a list of contributors contributing N% of all commits.
+    @apiParam {string} repo_group_id Repository Group ID.
+    @apiParam {string} repo_id Repository ID.
+    @apiParam {string} [year] Specify the year to return the results for. Default value: `current year`
+    @apiParam {string} [threshold=0.5] Specify N%. Accepts a value between `0` & `1` where `0` specifies
+                                        `0%` and `1` specifies `100%`.
+    @apiSuccessExample {json} Success-Response:
+                    [
+                        {
+                            "repo_id": 21334,
+                            "repo_name": "graphql",
+                            "email": "caniszczyk@gmail.com",
+                            "commits": 4
+                        },
+                        {
+                            "repo_id": 21334,
+                            "repo_name": "graphql",
+                            "email": "richard.j.schulte@gmail.com",
+                            "commits": 3
+                        },
+                        {
+                            "repo_id": "21334",
+                            "repo_name": "graphql",
+                            "email": "other_contributors",
+                            "commits": 5
+                        }
+                    ]
+    """
+    server.addRepoMetric(augur_db.top_committers, 'top-committers')


Test patch:
diff --git a/augur/datasources/augur_db/test_augur_db.py b/augur/datasources/augur_db/test_augur_db.py
--- a/augur/datasources/augur_db/test_augur_db.py
+++ b/augur/datasources/augur_db/test_augur_db.py
@@ -231,6 +231,15 @@ def test_cii_best_practices_badge(augur_db):
     # repo_group
     assert augur_db.cii_best_practices_badge(21).iloc[0]['tiered_percentage'] > 80
 
+def test_average_issue_resolution_time(augur_db):
+    #repo
+    assert augur_db.average_issue_resolution_time(24, 21464).isin(
+        ['maven-release', '276 days 13:54:13.2']).any().any()
+
+    # repo_group
+    assert augur_db.average_issue_resolution_time(24).isin(
+        ['maven-release', '276 days 13:54:13.2']).any().any()
+
 def test_languages(augur_db):
     # TODO
     pass
@@ -253,8 +262,8 @@ def test_annual_lines_of_code_count_ranked_by_repo_in_repo_group(augur_db):
     assert augur_db.annual_lines_of_code_count_ranked_by_repo_in_repo_group(20, 21000,timeframe = 'year').iloc[0].net > 0
 
 def test_annual_lines_of_code_count_ranked_by_new_repo_in_repo_group(augur_db):
-    assert augur_db.annual_lines_of_code_count_ranked_by_new_repo_in_repo_group(20).iloc[0].net > 0 
-    assert augur_db.annual_lines_of_code_count_ranked_by_new_repo_in_repo_group(20, 21000).iloc[0].net > 0 
+    assert augur_db.annual_lines_of_code_count_ranked_by_new_repo_in_repo_group(20).iloc[0].net > 0
+    assert augur_db.annual_lines_of_code_count_ranked_by_new_repo_in_repo_group(20, 21000).iloc[0].net > 0
 
 def test_annual_commit_count_ranked_by_repo_in_repo_group(augur_db):
     assert augur_db.annual_commit_count_ranked_by_repo_in_repo_group(20).iloc[0].net > 0
@@ -263,8 +272,16 @@ def test_annual_commit_count_ranked_by_repo_in_repo_group(augur_db):
     assert augur_db.annual_commit_count_ranked_by_repo_in_repo_group(20, 21000,timeframe = 'year').iloc[0].net > 0
 
 def test_annual_commit_count_ranked_by_new_repo_in_repo_group(augur_db):
-    assert augur_db.annual_commit_count_ranked_by_new_repo_in_repo_group(20).iloc[0].net > 0 
-    assert augur_db.annual_commit_count_ranked_by_new_repo_in_repo_group(20, 21000).iloc[0].net > 0 
+    assert augur_db.annual_commit_count_ranked_by_new_repo_in_repo_group(20).iloc[0].net > 0
+    assert augur_db.annual_commit_count_ranked_by_new_repo_in_repo_group(20, 21000).iloc[0].net > 0
+
+def test_top_committers(augur_db):
+    assert augur_db.top_committers(20).iloc[0]['commits'] > 0
+    assert augur_db.top_committers(20, year=2017).iloc[0]['commits'] > 0
+    assert augur_db.top_committers(20, year=2017, threshold=0.7).iloc[0]['commits'] > 0
+    assert augur_db.top_committers(20, 21000).iloc[0]['commits'] > 0
+    assert augur_db.top_committers(20, 21000, year=2017).iloc[0]['commits'] > 0
+    assert augur_db.top_committers(20, 21000, year=2017, threshold=0.7).iloc[0]['commits'] > 0
 
 def test_get_repos_for_dosocs(augur_db):
     assert augur_db.get_repos_for_dosocs().isin(
diff --git a/augur/datasources/augur_db/test_augur_db_routes.py b/augur/datasources/augur_db/test_augur_db_routes.py
--- a/augur/datasources/augur_db/test_augur_db_routes.py
+++ b/augur/datasources/augur_db/test_augur_db_routes.py
@@ -344,6 +344,18 @@ def test_cii_best_practices_badge_by_repo(augur_db_routes):
     assert response.status_code == 200
     assert len(data) >= 1
 
+def test_average_issue_resolution_time_by_group(augur_db_routes):
+    response = requests.get('http://localhost:5000/api/unstable/repo-groups/24/average-issue-resolution-time')
+    data = response.json()
+    assert response.status_code == 200
+    assert len(data) > 0
+
+def test_average_issue_resolution_time_by_repo(augur_db_routes):
+    response = requests.get('http://localhost:5000/api/unstable/repo-groups/24/repos/21464/average-issue-resolution-time')
+    data = response.json()
+    assert response.status_code == 200
+    assert len(data) > 0
+
 def test_languages_by_group(augur_db_routes):
     # TODO need data
     pass
@@ -428,6 +440,20 @@ def test_annual_lines_of_code_count_ranked_by_repo_in_repo_group_by_group(augur_
     assert len(data) >= 1
     assert data[0]["net"] > 0
 
+def test_top_committers_by_repo(augur_db_routes):
+    response = requests.get('http://0.0.0.0:5000/api/unstable/repo-groups/22/repos/21334/top-committers')
+    data = response.json()
+    assert response.status_code == 200
+    assert len(data) >= 1
+    assert data[0]['commits'] > 0
+
+def test_top_committers_by_group(augur_db_routes):
+    response = requests.get('http://0.0.0.0:5000/api/unstable/repo-groups/22/top-committers')
+    data = response.json()
+    assert response.status_code == 200
+    assert len(data) >= 1
+    assert data[0]['commits'] > 0
+
 def test_committer_by_repo(augur_db_routes):
     response = requests.get('http://localhost:5000/api/unstable/repo-groups/21/repos/21222/committers')
     data = response.json()