Completed
Pull Request — master (#45)
by Gonzalo
46s
created

GitHubRepo.milestone()   A

Complexity

Conditions 4

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 4
c 6
b 0
f 0
dl 0
loc 19
rs 9.2
1
# -*- coding: utf-8 -*-
2
# -----------------------------------------------------------------------------
3
# Copyright (c) The Spyder Development Team
4
#
5
# Licensed under the terms of the MIT License
6
# (See LICENSE.txt for details)
7
# -----------------------------------------------------------------------------
8
"""Github repo wrapper."""
9
10
from __future__ import print_function
11
12
# Standard library imports
13
import datetime
14
import sys
15
16
# Local imports
17
from loghub.external.github import ApiError, ApiNotFoundError, GitHub
18
19
20
class GitHubRepo(object):
21
    """Github repository wrapper."""
22
23
    def __init__(self, username=None, password=None, token=None, repo=None):
24
        """Github repository wrapper."""
25
        self._username = username
26
        self._password = password
27
        self._token = token
28
29
        self.gh = GitHub(
30
            username=username,
31
            password=password,
32
            access_token=token, )
33
        repo_organization, repo_name = repo.split('/')
34
        self._repo_organization = repo_organization
35
        self._repo_name = repo_name
36
        self.repo = self.gh.repos(repo_organization)(repo_name)
37
38
        # Check username and repo name
39
        self._check_user()
40
        self._check_repo_name()
41
42
    def _check_user(self):
43
        """Check if the supplied username is valid."""
44
        try:
45
            self.gh.users(self._repo_organization).get()
46
        except ApiNotFoundError as error:
47
            print('LOGHUB: Organization/user `{}` seems to be '
48
                  'invalid.\n'.format(self._repo_organization))
49
            sys.exit(1)
50
        except ApiError as error:
51
            self._check_rate()
52
            print('LOGHUB: The credentials seems to be invalid!\n')
53
            sys.exit(1)
54
55
    def _check_repo_name(self):
56
        """Check if the supplied repository exists."""
57
        try:
58
            self.repo.get()
59
        except ApiNotFoundError as error:
60
            print('LOGHUB: Repository `{0}` for organization/username `{1}` '
61
                  'seems to be invalid.\n'.format(self._repo_name,
62
                                                  self._repo_organization))
63
            sys.exit(1)
64
        except ApiError as error:
65
            self._check_rate()
66
67
    def _check_rate(self):
68
        """Check and handle if api rate limit has been exceeded."""
69
        if self.gh.x_ratelimit_remaining == 0:
70
            print('LOGHUB: GitHub API rate limit exceeded!\n')
71
            if not self._username and not self._password or not self._token:
72
                print('LOGHUB: Try running loghub with user/password or '
73
                      'a valid token.\n')
74
            sys.exit(1)
75
76
    def _filter_since(self, issues, since):
77
        """Filter out all issues before `since` date."""
78
        if since:
79
            since_date = self.str_to_date(since)
80
            for issue in issues[:]:
81
                close_date = self.str_to_date(issue['closed_at'])
82
                if close_date < since_date and issue in issues:
83
                    issues.remove(issue)
84
        return issues
85
86
    def _filter_until(self, issues, until):
87
        """Filter out all issues after `until` date."""
88
        if until:
89
            until_date = self.str_to_date(until)
90
            for issue in issues[:]:
91
                close_date = self.str_to_date(issue['closed_at'])
92
                if close_date > until_date and issue in issues:
93
                    issues.remove(issue)
94
        return issues
95
96
    def _filer_closed_prs(self, issues, branch):
97
        """Filter out closed PRs."""
98
        for issue in issues[:]:
99
            pr = issue.get('pull_request', '')
100
101
            # Add label names inside additional key
102
            issue['loghub_label_names'] = [
103
                l['name'] for l in issue.get('labels')
104
            ]
105
106
            if pr:
107
                number = issue['number']
108
                if not self.is_merged(number) and issue in issues:
109
                    issues.remove(issue)
110
111
                if branch:
112
                    # Get PR info and get base branch
113
                    pr_data = self.pr(number)
114
                    base_ref = pr_data['base']['ref']
115
                    if base_ref != branch and issue in issues:
116
                        issues.remove(issue)
117
        return issues
118
119
    def tags(self):
120
        """Return all tags."""
121
        self._check_rate()
122
        return self.repo('git')('refs')('tags').get()
123
124
    def tag(self, tag_name):
125
        """Get tag information."""
126
        self._check_rate()
127
        refs = self.repo('git')('refs')('tags').get()
128
        sha = -1
129
130
        tags = []
131
        for ref in refs:
132
            ref_name = 'refs/tags/{tag}'.format(tag=tag_name)
133
            if 'object' in ref and ref['ref'] == ref_name:
134
                sha = ref['object']['sha']
135
            tags.append(ref['ref'].split('/')[-1])
136
137
        if sha == -1:
138
            print("LOGHUB: You didn't pass a valid tag name!")
139
            print('LOGHUB: The available tags are: {0}\n'.format(tags))
140
            sys.exit(1)
141
142
        return self.repo('git')('tags')(sha).get()
143
144
    def milestones(self):
145
        """Return all milestones."""
146
        self._check_rate()
147
        return self.repo.milestones.get(state='all')
148
149
    def milestone(self, milestone_title):
150
        """Return milestone with given title."""
151
        self._check_rate()
152
        milestones = self.milestones()
153
        milestone_number = -1
154
155
        milestone_titles = []
156
        for milestone in milestones:
157
            if milestone['title'] == milestone_title:
158
                milestone_number = milestone['number']
159
            milestone_titles.append(milestone['title'])
160
161
        if milestone_number == -1:
162
            print("LOGHUB: You didn't pass a valid milestone name!")
163
            print('LOGHUB: The available milestones are: {0}\n'
164
                  ''.format(milestone_titles))
165
            sys.exit(1)
166
167
        return milestone
168
169
    def pr(self, pr_number):
170
        """Get PR information."""
171
        self._check_rate()
172
        return self.repo('pulls')(str(pr_number)).get()
173
174
    def issues(self,
175
               milestone=None,
176
               state=None,
177
               assignee=None,
178
               creator=None,
179
               mentioned=None,
180
               labels=None,
181
               sort=None,
182
               direction=None,
183
               since=None,
184
               until=None,
185
               branch=None):
186
        """Return Issues and Pull Requests."""
187
        self._check_rate()
188
        page = 1
189
        issues = []
190
        while True:
191
            result = self.repo.issues.get(page=page,
192
                                          per_page=100,
193
                                          milestone=milestone,
194
                                          state=state,
195
                                          assignee=assignee,
196
                                          creator=creator,
197
                                          mentioned=mentioned,
198
                                          labels=labels,
199
                                          sort=sort,
200
                                          direction=direction,
201
                                          since=since)
202
            if len(result) > 0:
203
                issues += result
204
                page = page + 1
205
            else:
206
                break
207
208
        # If since was provided, filter the issue
209
        issues = self._filter_since(issues, since)
210
211
        # If until was provided, filter the issue
212
        issues = self._filter_until(issues, until)
213
214
        # If it is a pr check if it is merged or closed, removed closed ones
215
        issues = self._filer_closed_prs(issues, branch)
216
217
        return issues
218
219
    def is_merged(self, pr):
220
        """
221
        Return wether a PR was merged, or if it was closed and discarded.
222
223
        https://developer.github.com/v3/pulls/#get-if-a-pull-request-has-been-merged
224
        """
225
        self._check_rate()
226
        merged = True
227
        try:
228
            self.repo('pulls')(str(pr))('merge').get()
229
        except Exception:
230
            merged = False
231
        return merged
232
233
    @staticmethod
234
    def str_to_date(string):
235
        """Convert ISO date string to datetime object."""
236
        parts = string.split('T')
237
        date_parts = parts[0]
238
        time_parts = parts[1][:-1]
239
        year, month, day = [int(i) for i in date_parts.split('-')]
240
        hour, minutes, seconds = [int(i) for i in time_parts.split(':')]
241
        return datetime.datetime(year, month, day, hour, minutes, seconds)
242