szha commented on a change in pull request #27:
URL: https://github.com/apache/incubator-mxnet-ci/pull/27#discussion_r484130385



##########
File path: services/lambda-pr-status-labeler/pr_status_bot/PRStatusBot.py
##########
@@ -0,0 +1,355 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import ast
+import json
+import hmac
+import hashlib
+import os
+import logging
+import re
+import secret_manager
+
+from github import Github
+
+# Define the constants
+# Github labels
+PR_WORK_IN_PROGRESS_LABEL = 'pr-work-in-progress'
+PR_AWAITING_TESTING_LABEL = 'pr-awaiting-testing'
+PR_AWAITING_MERGE_LABEL = 'pr-awaiting-merge'
+PR_AWAITING_REVIEW_LABEL = 'pr-awaiting-review'
+PR_AWAITING_RESPONSE_LABEL = 'pr-awaiting-response'
+
+WORK_IN_PROGRESS_TITLE_SUBSTRING = 'WIP'
+
+# CI state
+FAILURE_STATE = 'failure'
+PENDING_STATE = 'pending'
+
+# Review state
+APPROVED_STATE = 'APPROVED'
+CHANGES_REQUESTED_STATE = 'CHANGES_REQUESTED'
+COMMENTED_STATE = 'COMMENTED'
+DISMISSED_STATE = 'DISMISSED'
+
+
+class PRStatusBot:
+    def __init__(self,
+                 repo=os.environ.get("repo"),
+                 github_personal_access_token=None,
+                 apply_secret=True):
+        """
+        Initializes the PR Status Bot
+        :param repo: GitHub repository that is being referenced
+        :param github_personal_access_token: GitHub authentication token 
(Personal access token)
+        :param apply_secret: GitHub secret credential (Secret credential that 
is unique to a GitHub developer)
+        """
+        self.repo = repo
+        self.github_personal_access_token = github_personal_access_token
+        if apply_secret:
+            self._get_secret()
+
+    def _get_secret(self):
+        """
+        This method is to get secret value from Secrets Manager
+        """
+        secret = json.loads(secret_manager.get_secret())
+        self.github_personal_access_token = 
secret["github_personal_access_token"]
+        self.webhook_secret = secret["webhook_secret"]
+
+    def _secure_webhook(self, event):
+        """
+        This method will validate the security of the webhook, it confirms 
that the secret
+        of the webhook is matched and that each github event is signed 
appropriately
+        :param event: The github event we want to validate
+        :return Response denoting success or failure of security
+        """
+
+        # Validating github event is signed
+        try:
+            git_signed = 
ast.literal_eval(event["Records"][0]['body'])['headers']["X-Hub-Signature"]
+        except KeyError:
+            raise Exception("WebHook from GitHub is not signed")
+        git_signed = git_signed.replace('sha1=', '')
+
+        # Signing our event with the same secret as what we assigned to github 
event
+        secret = self.webhook_secret
+        body = ast.literal_eval(event["Records"][0]['body'])['body']
+        secret_sign = hmac.new(key=secret.encode('utf-8'), 
msg=body.encode('utf-8'), digestmod=hashlib.sha1).hexdigest()
+
+        # Validating signatures match
+        return hmac.compare_digest(git_signed, secret_sign)
+
+    def _get_github_object(self):
+        """
+        This method returns github object initialized with Github personal 
access token
+        """
+        github_obj = Github(self.github_personal_access_token)
+        return github_obj
+
+    def _get_pull_request_object(self, github_obj, pr_number):
+        """
+        This method returns a PullRequest object based on the PR number
+        :param github_obj
+        :param pr_number
+        """
+        repo = github_obj.get_repo(self.repo)
+        pr_obj = repo.get_pull(int(pr_number))
+        return pr_obj
+
+    def _get_commit_object(self, github_obj, commit_sha):
+        """
+        This method returns a Commit object based on the SHA of the commit
+        :param github_obj
+        :param commit_sha
+        """
+        repo = github_obj.get_repo(self.repo)
+        commit_obj = repo.get_commit(commit_sha)
+        return commit_obj
+
+    def _is_mxnet_committer(self, github_obj, reviewer):
+        """
+        This method checks if the Pull Request reviewer is a member of MXNet 
committers
+        It uses the Github API for fetching team members of a repo
+        Only a Committer can access [read/write] to Apache MXNet Committer 
team on Github
+        Retrieved the Team ID of the Apache MXNet Committer team on Github 
using a Committer's credentials
+        """
+        team = github_obj.get_organization('apache').get_team(2413476)
+        return team.has_in_members(reviewer)
+
+    def _drop_other_pr_labels(self, pr, desired_label):
+        labels = pr.get_labels()
+        if not labels:
+            logging.info('No labels found')
+            return
+
+        for label in labels:
+            logging.info(f'Label:{label}')
+            if label.name.startswith('pr-') and label.name != desired_label:
+                try:
+                    logging.info(f'Removing {label}')
+                    # pr.remove_from_labels(label)
+                except Exception:
+                    logging.error(f'Error while removing the label {label}')
+
+    def _add_label(self, pr, label):
+        # drop other PR labels
+        self._drop_other_pr_labels(pr, label)
+
+        # check if the PR already has the desired label
+        if(self._has_desired_label(pr, label)):

Review comment:
       would you be able to always assign the appropriate label instead? this 
will make the operation idempotent.

##########
File path: services/lambda-pr-status-labeler/pr_status_bot/PRStatusBot.py
##########
@@ -0,0 +1,355 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import ast
+import json
+import hmac
+import hashlib
+import os
+import logging
+import re
+import secret_manager
+
+from github import Github
+
+# Define the constants
+# Github labels
+PR_WORK_IN_PROGRESS_LABEL = 'pr-work-in-progress'
+PR_AWAITING_TESTING_LABEL = 'pr-awaiting-testing'
+PR_AWAITING_MERGE_LABEL = 'pr-awaiting-merge'
+PR_AWAITING_REVIEW_LABEL = 'pr-awaiting-review'
+PR_AWAITING_RESPONSE_LABEL = 'pr-awaiting-response'
+
+WORK_IN_PROGRESS_TITLE_SUBSTRING = 'WIP'
+
+# CI state
+FAILURE_STATE = 'failure'
+PENDING_STATE = 'pending'
+
+# Review state
+APPROVED_STATE = 'APPROVED'
+CHANGES_REQUESTED_STATE = 'CHANGES_REQUESTED'
+COMMENTED_STATE = 'COMMENTED'
+DISMISSED_STATE = 'DISMISSED'
+
+
+class PRStatusBot:
+    def __init__(self,
+                 repo=os.environ.get("repo"),
+                 github_personal_access_token=None,
+                 apply_secret=True):
+        """
+        Initializes the PR Status Bot
+        :param repo: GitHub repository that is being referenced
+        :param github_personal_access_token: GitHub authentication token 
(Personal access token)
+        :param apply_secret: GitHub secret credential (Secret credential that 
is unique to a GitHub developer)
+        """
+        self.repo = repo
+        self.github_personal_access_token = github_personal_access_token
+        if apply_secret:
+            self._get_secret()
+
+    def _get_secret(self):
+        """
+        This method is to get secret value from Secrets Manager
+        """
+        secret = json.loads(secret_manager.get_secret())
+        self.github_personal_access_token = 
secret["github_personal_access_token"]
+        self.webhook_secret = secret["webhook_secret"]
+
+    def _secure_webhook(self, event):
+        """
+        This method will validate the security of the webhook, it confirms 
that the secret
+        of the webhook is matched and that each github event is signed 
appropriately
+        :param event: The github event we want to validate
+        :return Response denoting success or failure of security
+        """
+
+        # Validating github event is signed
+        try:
+            git_signed = 
ast.literal_eval(event["Records"][0]['body'])['headers']["X-Hub-Signature"]
+        except KeyError:
+            raise Exception("WebHook from GitHub is not signed")
+        git_signed = git_signed.replace('sha1=', '')
+
+        # Signing our event with the same secret as what we assigned to github 
event
+        secret = self.webhook_secret
+        body = ast.literal_eval(event["Records"][0]['body'])['body']
+        secret_sign = hmac.new(key=secret.encode('utf-8'), 
msg=body.encode('utf-8'), digestmod=hashlib.sha1).hexdigest()
+
+        # Validating signatures match
+        return hmac.compare_digest(git_signed, secret_sign)
+
+    def _get_github_object(self):
+        """
+        This method returns github object initialized with Github personal 
access token
+        """
+        github_obj = Github(self.github_personal_access_token)
+        return github_obj
+
+    def _get_pull_request_object(self, github_obj, pr_number):
+        """
+        This method returns a PullRequest object based on the PR number
+        :param github_obj
+        :param pr_number
+        """
+        repo = github_obj.get_repo(self.repo)
+        pr_obj = repo.get_pull(int(pr_number))
+        return pr_obj
+
+    def _get_commit_object(self, github_obj, commit_sha):
+        """
+        This method returns a Commit object based on the SHA of the commit
+        :param github_obj
+        :param commit_sha
+        """
+        repo = github_obj.get_repo(self.repo)
+        commit_obj = repo.get_commit(commit_sha)
+        return commit_obj
+
+    def _is_mxnet_committer(self, github_obj, reviewer):
+        """
+        This method checks if the Pull Request reviewer is a member of MXNet 
committers
+        It uses the Github API for fetching team members of a repo
+        Only a Committer can access [read/write] to Apache MXNet Committer 
team on Github
+        Retrieved the Team ID of the Apache MXNet Committer team on Github 
using a Committer's credentials
+        """
+        team = github_obj.get_organization('apache').get_team(2413476)
+        return team.has_in_members(reviewer)
+
+    def _drop_other_pr_labels(self, pr, desired_label):
+        labels = pr.get_labels()
+        if not labels:
+            logging.info('No labels found')
+            return
+
+        for label in labels:
+            logging.info(f'Label:{label}')
+            if label.name.startswith('pr-') and label.name != desired_label:
+                try:
+                    logging.info(f'Removing {label}')
+                    # pr.remove_from_labels(label)
+                except Exception:
+                    logging.error(f'Error while removing the label {label}')
+
+    def _add_label(self, pr, label):
+        # drop other PR labels
+        self._drop_other_pr_labels(pr, label)
+
+        # check if the PR already has the desired label
+        if(self._has_desired_label(pr, label)):
+            logging.info(f'PR {pr.number} already contains the label {label}')
+            return
+
+        logging.info(f'BOT Labels: {label}')
+        # try:
+        #     pr.add_to_labels(label)
+        # except Exception:
+        #     logging.error(f'Unable to add label {label}')
+
+        # verify that label has been correctly added
+        # if(self._has_desired_label(pr, label)):
+        #     logging.info(f'Successfully labeled {label} for PR-{pr.number}')
+        return
+
+    def _has_desired_label(self, pr, desired_label):
+        """
+        This method returns True if desired label found in PR labels
+        """
+        labels = pr.get_labels()
+        for label in labels:
+            if desired_label == label.name:
+                return True
+        return False
+
+    def _get_reviewer(self, review):
+        """
+        This method returns the reviewer of a particular PR review
+        :param PullRequestReview
+        :return NamedUser
+        """
+        return review.user
+
+    def _parse_reviews(self, github_obj, pr):
+        """
+        This method parses through the reviews of the PR and returns count of
+        4 states: Approved reviews, Comment reviews, Requested Changes reviews
+        and Dismissed reviews
+        Note: Only reviews by MXNet Committers are considered.
+        :param github_obj
+        :param pr
+        """
+        approved_count, requested_changes_count, comment_count, 
dismissed_count = 0, 0, 0, 0
+        for review in pr.get_reviews():
+            # continue if the review is by non-committer
+            reviewer = self._get_reviewer(review)
+            if not self._is_mxnet_committer(github_obj, reviewer):
+                logging.info(f'Review is by non-MXNet Committer: {reviewer}. 
Ignore.')
+                continue
+
+            if review.state == APPROVED_STATE:
+                approved_count += 1
+            elif review.state == CHANGES_REQUESTED_STATE:
+                requested_changes_count += 1
+            elif review.state == COMMENTED_STATE:
+                comment_count += 1
+            elif review.state == DISMISSED_STATE:
+                dismissed_count += 1
+            else:
+                logging.error(f'Unknown review state {review.state}')

Review comment:
       extract to a method

##########
File path: services/lambda-pr-status-labeler/pr_status_bot/PRStatusBot.py
##########
@@ -0,0 +1,355 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import ast
+import json
+import hmac
+import hashlib
+import os
+import logging
+import re
+import secret_manager
+
+from github import Github
+
+# Define the constants
+# Github labels
+PR_WORK_IN_PROGRESS_LABEL = 'pr-work-in-progress'
+PR_AWAITING_TESTING_LABEL = 'pr-awaiting-testing'
+PR_AWAITING_MERGE_LABEL = 'pr-awaiting-merge'
+PR_AWAITING_REVIEW_LABEL = 'pr-awaiting-review'
+PR_AWAITING_RESPONSE_LABEL = 'pr-awaiting-response'
+
+WORK_IN_PROGRESS_TITLE_SUBSTRING = 'WIP'
+
+# CI state
+FAILURE_STATE = 'failure'
+PENDING_STATE = 'pending'
+
+# Review state
+APPROVED_STATE = 'APPROVED'
+CHANGES_REQUESTED_STATE = 'CHANGES_REQUESTED'
+COMMENTED_STATE = 'COMMENTED'
+DISMISSED_STATE = 'DISMISSED'
+
+
+class PRStatusBot:
+    def __init__(self,
+                 repo=os.environ.get("repo"),
+                 github_personal_access_token=None,
+                 apply_secret=True):
+        """
+        Initializes the PR Status Bot
+        :param repo: GitHub repository that is being referenced
+        :param github_personal_access_token: GitHub authentication token 
(Personal access token)
+        :param apply_secret: GitHub secret credential (Secret credential that 
is unique to a GitHub developer)
+        """
+        self.repo = repo
+        self.github_personal_access_token = github_personal_access_token
+        if apply_secret:
+            self._get_secret()
+
+    def _get_secret(self):
+        """
+        This method is to get secret value from Secrets Manager
+        """
+        secret = json.loads(secret_manager.get_secret())
+        self.github_personal_access_token = 
secret["github_personal_access_token"]
+        self.webhook_secret = secret["webhook_secret"]
+
+    def _secure_webhook(self, event):
+        """
+        This method will validate the security of the webhook, it confirms 
that the secret
+        of the webhook is matched and that each github event is signed 
appropriately
+        :param event: The github event we want to validate
+        :return Response denoting success or failure of security
+        """
+
+        # Validating github event is signed
+        try:
+            git_signed = 
ast.literal_eval(event["Records"][0]['body'])['headers']["X-Hub-Signature"]
+        except KeyError:
+            raise Exception("WebHook from GitHub is not signed")
+        git_signed = git_signed.replace('sha1=', '')
+
+        # Signing our event with the same secret as what we assigned to github 
event
+        secret = self.webhook_secret
+        body = ast.literal_eval(event["Records"][0]['body'])['body']
+        secret_sign = hmac.new(key=secret.encode('utf-8'), 
msg=body.encode('utf-8'), digestmod=hashlib.sha1).hexdigest()
+
+        # Validating signatures match
+        return hmac.compare_digest(git_signed, secret_sign)
+
+    def _get_github_object(self):
+        """
+        This method returns github object initialized with Github personal 
access token
+        """
+        github_obj = Github(self.github_personal_access_token)
+        return github_obj
+
+    def _get_pull_request_object(self, github_obj, pr_number):
+        """
+        This method returns a PullRequest object based on the PR number
+        :param github_obj
+        :param pr_number
+        """
+        repo = github_obj.get_repo(self.repo)
+        pr_obj = repo.get_pull(int(pr_number))
+        return pr_obj
+
+    def _get_commit_object(self, github_obj, commit_sha):
+        """
+        This method returns a Commit object based on the SHA of the commit
+        :param github_obj
+        :param commit_sha
+        """
+        repo = github_obj.get_repo(self.repo)
+        commit_obj = repo.get_commit(commit_sha)
+        return commit_obj
+
+    def _is_mxnet_committer(self, github_obj, reviewer):
+        """
+        This method checks if the Pull Request reviewer is a member of MXNet 
committers
+        It uses the Github API for fetching team members of a repo
+        Only a Committer can access [read/write] to Apache MXNet Committer 
team on Github
+        Retrieved the Team ID of the Apache MXNet Committer team on Github 
using a Committer's credentials
+        """
+        team = github_obj.get_organization('apache').get_team(2413476)
+        return team.has_in_members(reviewer)
+
+    def _drop_other_pr_labels(self, pr, desired_label):
+        labels = pr.get_labels()
+        if not labels:
+            logging.info('No labels found')
+            return
+
+        for label in labels:
+            logging.info(f'Label:{label}')
+            if label.name.startswith('pr-') and label.name != desired_label:
+                try:
+                    logging.info(f'Removing {label}')
+                    # pr.remove_from_labels(label)
+                except Exception:
+                    logging.error(f'Error while removing the label {label}')
+
+    def _add_label(self, pr, label):
+        # drop other PR labels
+        self._drop_other_pr_labels(pr, label)
+
+        # check if the PR already has the desired label
+        if(self._has_desired_label(pr, label)):
+            logging.info(f'PR {pr.number} already contains the label {label}')
+            return
+
+        logging.info(f'BOT Labels: {label}')
+        # try:
+        #     pr.add_to_labels(label)
+        # except Exception:
+        #     logging.error(f'Unable to add label {label}')
+
+        # verify that label has been correctly added
+        # if(self._has_desired_label(pr, label)):
+        #     logging.info(f'Successfully labeled {label} for PR-{pr.number}')
+        return
+
+    def _has_desired_label(self, pr, desired_label):
+        """
+        This method returns True if desired label found in PR labels
+        """
+        labels = pr.get_labels()

Review comment:
       `return desired_label in set(pr.get_labels())`

##########
File path: services/lambda-pr-status-labeler/pr_status_bot/PRStatusBot.py
##########
@@ -0,0 +1,355 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import ast
+import json
+import hmac
+import hashlib
+import os
+import logging
+import re
+import secret_manager
+
+from github import Github
+
+# Define the constants
+# Github labels
+PR_WORK_IN_PROGRESS_LABEL = 'pr-work-in-progress'
+PR_AWAITING_TESTING_LABEL = 'pr-awaiting-testing'
+PR_AWAITING_MERGE_LABEL = 'pr-awaiting-merge'
+PR_AWAITING_REVIEW_LABEL = 'pr-awaiting-review'
+PR_AWAITING_RESPONSE_LABEL = 'pr-awaiting-response'
+
+WORK_IN_PROGRESS_TITLE_SUBSTRING = 'WIP'
+
+# CI state
+FAILURE_STATE = 'failure'
+PENDING_STATE = 'pending'
+
+# Review state
+APPROVED_STATE = 'APPROVED'
+CHANGES_REQUESTED_STATE = 'CHANGES_REQUESTED'
+COMMENTED_STATE = 'COMMENTED'
+DISMISSED_STATE = 'DISMISSED'
+
+
+class PRStatusBot:
+    def __init__(self,
+                 repo=os.environ.get("repo"),
+                 github_personal_access_token=None,
+                 apply_secret=True):
+        """
+        Initializes the PR Status Bot
+        :param repo: GitHub repository that is being referenced
+        :param github_personal_access_token: GitHub authentication token 
(Personal access token)
+        :param apply_secret: GitHub secret credential (Secret credential that 
is unique to a GitHub developer)
+        """
+        self.repo = repo
+        self.github_personal_access_token = github_personal_access_token
+        if apply_secret:
+            self._get_secret()
+
+    def _get_secret(self):
+        """
+        This method is to get secret value from Secrets Manager
+        """
+        secret = json.loads(secret_manager.get_secret())
+        self.github_personal_access_token = 
secret["github_personal_access_token"]
+        self.webhook_secret = secret["webhook_secret"]
+
+    def _secure_webhook(self, event):
+        """
+        This method will validate the security of the webhook, it confirms 
that the secret
+        of the webhook is matched and that each github event is signed 
appropriately
+        :param event: The github event we want to validate
+        :return Response denoting success or failure of security
+        """
+
+        # Validating github event is signed
+        try:
+            git_signed = 
ast.literal_eval(event["Records"][0]['body'])['headers']["X-Hub-Signature"]
+        except KeyError:
+            raise Exception("WebHook from GitHub is not signed")
+        git_signed = git_signed.replace('sha1=', '')
+
+        # Signing our event with the same secret as what we assigned to github 
event
+        secret = self.webhook_secret
+        body = ast.literal_eval(event["Records"][0]['body'])['body']
+        secret_sign = hmac.new(key=secret.encode('utf-8'), 
msg=body.encode('utf-8'), digestmod=hashlib.sha1).hexdigest()
+
+        # Validating signatures match
+        return hmac.compare_digest(git_signed, secret_sign)
+
+    def _get_github_object(self):

Review comment:
       move to constructor

##########
File path: services/lambda-pr-status-labeler/pr_status_bot/PRStatusBot.py
##########
@@ -0,0 +1,355 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import ast
+import json
+import hmac
+import hashlib
+import os
+import logging
+import re
+import secret_manager
+
+from github import Github
+
+# Define the constants
+# Github labels
+PR_WORK_IN_PROGRESS_LABEL = 'pr-work-in-progress'
+PR_AWAITING_TESTING_LABEL = 'pr-awaiting-testing'
+PR_AWAITING_MERGE_LABEL = 'pr-awaiting-merge'
+PR_AWAITING_REVIEW_LABEL = 'pr-awaiting-review'
+PR_AWAITING_RESPONSE_LABEL = 'pr-awaiting-response'
+
+WORK_IN_PROGRESS_TITLE_SUBSTRING = 'WIP'
+
+# CI state
+FAILURE_STATE = 'failure'
+PENDING_STATE = 'pending'
+
+# Review state
+APPROVED_STATE = 'APPROVED'
+CHANGES_REQUESTED_STATE = 'CHANGES_REQUESTED'
+COMMENTED_STATE = 'COMMENTED'
+DISMISSED_STATE = 'DISMISSED'
+
+
+class PRStatusBot:
+    def __init__(self,
+                 repo=os.environ.get("repo"),
+                 github_personal_access_token=None,
+                 apply_secret=True):
+        """
+        Initializes the PR Status Bot
+        :param repo: GitHub repository that is being referenced
+        :param github_personal_access_token: GitHub authentication token 
(Personal access token)
+        :param apply_secret: GitHub secret credential (Secret credential that 
is unique to a GitHub developer)
+        """
+        self.repo = repo
+        self.github_personal_access_token = github_personal_access_token
+        if apply_secret:
+            self._get_secret()
+
+    def _get_secret(self):
+        """
+        This method is to get secret value from Secrets Manager
+        """
+        secret = json.loads(secret_manager.get_secret())
+        self.github_personal_access_token = 
secret["github_personal_access_token"]
+        self.webhook_secret = secret["webhook_secret"]
+
+    def _secure_webhook(self, event):
+        """
+        This method will validate the security of the webhook, it confirms 
that the secret
+        of the webhook is matched and that each github event is signed 
appropriately
+        :param event: The github event we want to validate
+        :return Response denoting success or failure of security
+        """
+
+        # Validating github event is signed
+        try:
+            git_signed = 
ast.literal_eval(event["Records"][0]['body'])['headers']["X-Hub-Signature"]
+        except KeyError:
+            raise Exception("WebHook from GitHub is not signed")
+        git_signed = git_signed.replace('sha1=', '')
+
+        # Signing our event with the same secret as what we assigned to github 
event
+        secret = self.webhook_secret
+        body = ast.literal_eval(event["Records"][0]['body'])['body']
+        secret_sign = hmac.new(key=secret.encode('utf-8'), 
msg=body.encode('utf-8'), digestmod=hashlib.sha1).hexdigest()
+
+        # Validating signatures match
+        return hmac.compare_digest(git_signed, secret_sign)
+
+    def _get_github_object(self):
+        """
+        This method returns github object initialized with Github personal 
access token
+        """
+        github_obj = Github(self.github_personal_access_token)
+        return github_obj
+
+    def _get_pull_request_object(self, github_obj, pr_number):
+        """
+        This method returns a PullRequest object based on the PR number
+        :param github_obj
+        :param pr_number
+        """
+        repo = github_obj.get_repo(self.repo)
+        pr_obj = repo.get_pull(int(pr_number))
+        return pr_obj
+
+    def _get_commit_object(self, github_obj, commit_sha):
+        """
+        This method returns a Commit object based on the SHA of the commit
+        :param github_obj
+        :param commit_sha
+        """
+        repo = github_obj.get_repo(self.repo)
+        commit_obj = repo.get_commit(commit_sha)
+        return commit_obj
+
+    def _is_mxnet_committer(self, github_obj, reviewer):
+        """
+        This method checks if the Pull Request reviewer is a member of MXNet 
committers
+        It uses the Github API for fetching team members of a repo
+        Only a Committer can access [read/write] to Apache MXNet Committer 
team on Github
+        Retrieved the Team ID of the Apache MXNet Committer team on Github 
using a Committer's credentials
+        """
+        team = github_obj.get_organization('apache').get_team(2413476)
+        return team.has_in_members(reviewer)
+
+    def _drop_other_pr_labels(self, pr, desired_label):
+        labels = pr.get_labels()
+        if not labels:
+            logging.info('No labels found')
+            return
+
+        for label in labels:
+            logging.info(f'Label:{label}')
+            if label.name.startswith('pr-') and label.name != desired_label:
+                try:
+                    logging.info(f'Removing {label}')
+                    # pr.remove_from_labels(label)
+                except Exception:
+                    logging.error(f'Error while removing the label {label}')
+
+    def _add_label(self, pr, label):
+        # drop other PR labels
+        self._drop_other_pr_labels(pr, label)
+
+        # check if the PR already has the desired label
+        if(self._has_desired_label(pr, label)):
+            logging.info(f'PR {pr.number} already contains the label {label}')
+            return
+
+        logging.info(f'BOT Labels: {label}')
+        # try:
+        #     pr.add_to_labels(label)
+        # except Exception:
+        #     logging.error(f'Unable to add label {label}')
+
+        # verify that label has been correctly added
+        # if(self._has_desired_label(pr, label)):
+        #     logging.info(f'Successfully labeled {label} for PR-{pr.number}')
+        return
+
+    def _has_desired_label(self, pr, desired_label):
+        """
+        This method returns True if desired label found in PR labels
+        """
+        labels = pr.get_labels()
+        for label in labels:
+            if desired_label == label.name:
+                return True
+        return False
+
+    def _get_reviewer(self, review):
+        """
+        This method returns the reviewer of a particular PR review
+        :param PullRequestReview
+        :return NamedUser
+        """
+        return review.user

Review comment:
       is this really necessary for a method?

##########
File path: services/lambda-pr-status-labeler/pr_status_bot/PRStatusBot.py
##########
@@ -0,0 +1,355 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import ast
+import json
+import hmac
+import hashlib
+import os
+import logging
+import re
+import secret_manager
+
+from github import Github
+
+# Define the constants
+# Github labels
+PR_WORK_IN_PROGRESS_LABEL = 'pr-work-in-progress'
+PR_AWAITING_TESTING_LABEL = 'pr-awaiting-testing'
+PR_AWAITING_MERGE_LABEL = 'pr-awaiting-merge'
+PR_AWAITING_REVIEW_LABEL = 'pr-awaiting-review'
+PR_AWAITING_RESPONSE_LABEL = 'pr-awaiting-response'
+
+WORK_IN_PROGRESS_TITLE_SUBSTRING = 'WIP'
+
+# CI state
+FAILURE_STATE = 'failure'
+PENDING_STATE = 'pending'
+
+# Review state
+APPROVED_STATE = 'APPROVED'
+CHANGES_REQUESTED_STATE = 'CHANGES_REQUESTED'
+COMMENTED_STATE = 'COMMENTED'
+DISMISSED_STATE = 'DISMISSED'
+
+
+class PRStatusBot:
+    def __init__(self,
+                 repo=os.environ.get("repo"),
+                 github_personal_access_token=None,
+                 apply_secret=True):
+        """
+        Initializes the PR Status Bot
+        :param repo: GitHub repository that is being referenced
+        :param github_personal_access_token: GitHub authentication token 
(Personal access token)
+        :param apply_secret: GitHub secret credential (Secret credential that 
is unique to a GitHub developer)
+        """
+        self.repo = repo
+        self.github_personal_access_token = github_personal_access_token
+        if apply_secret:
+            self._get_secret()
+
+    def _get_secret(self):

Review comment:
       move to constructor




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to