Skip to content
Snippets Groups Projects
Select Git revision
  • b45c688db8c1a26a69e8d6a9ccff46dbc1286c66
  • master default protected
2 results

gitlab_classes.py

Blame
  • gitlab_classes.py 17.96 KiB
    from datetime import datetime
    from typing import ClassVar, Dict, Iterable, List, Optional, Set, Union
    
    from gitlab import Gitlab, MAINTAINER_ACCESS
    from gitlab.v4.objects import Issue, Project, ProjectCommit, User
    
    from config import Config
    
    
    class GitlabSession:
        __instance: ClassVar[Gitlab] = None
    
        @classmethod
        def get_session(cls) -> Gitlab:
            if cls.__instance is None:
                cls.__instance = Gitlab(Config.gitlab_url, private_token=Config.gitlab_api_key)
            return cls.__instance
    
    
    class GitlabUser:
        git_user: User
    
        def __init__(self, user: Union[int, str, User]):
            """
            Creates a User object, populating the backing git_user instance with the appropriate gitlab.User object
            :param user: must be either a gitlab.User object, an integer representing the user ID, or a string containing
            the username
            """
            super().__init__()
            if isinstance(user, int):  # by user id
                self.git_user = GitlabSession.get_session().users.get(user)
            elif isinstance(user, str):  # by username
                self.git_user = GitlabSession.get_session().users.list(username=user)[0]
            else:
                self.git_user = user
    
        def get_user_id(self) -> int:
            return self.git_user.id
    
        def get_name(self) -> str:
            return self.git_user.name
    
        def get_username(self) -> str:
            return self.git_user.username
    
        def get_email(self) -> Optional[str]:
            # return self.git_user.emails.list()[0]   # list() is forbidden
            try:
                return self.git_user.email
            except AttributeError:
                return None
    
        def get_site(self) -> str:
            return self.git_user.web_url
    
        def __repr__(self) -> str:
            username = self.get_username()
            return f'@{username}'
    
        def __eq__(self, other: "GitlabUser") -> bool:
            # if isinstance(other, GitlabUser):
            return self.get_username() == other.get_username()
            # else:
            # return False
    
        def __ne__(self, other: "GitlabUser") -> bool:
            return not self.__eq__(other)
    
    
    class GitlabIssue:
        git_issue: Issue
    
        def __init__(self, issue: Issue):
            """
            Creates an Issue object, populating the backing git_issue instance with the appropriate gitlab.Issue object
            :param issue: must be a gitlab.Issue object
            """
            super().__init__()
            self.git_issue = issue
    
        def get_universal_issue_id(self) -> int:
            """
            :return: universally-unique identifier
            """
            return self.git_issue.id
    
        def get_project_issue_id(self) -> int:
            """
            :return: project-unique identifier
            """
            return self.git_issue.iid
    
        def get_title(self) -> str:
            """
            :return: issue's title
            """
            return self.git_issue.title
    
        def get_description(self) -> str:
            """
            :return: issue's description
            """
            return self.git_issue.description
    
        def get_state(self) -> str:
            """
            :return: opened or closed
            """
            return self.git_issue.state
    
        def get_created_at(self) -> datetime:
            """
            :return: an "aware" datetime object representing the creation date/time
            """
            created_at: str = self.git_issue.created_at
            if created_at[-1] in ('z', 'Z'):  # Didn't encounter this problem with created_at
                created_at = created_at[:-1] + '+00:00'
            return datetime.fromisoformat(created_at)
    
        def get_updated_at(self) -> datetime:
            """
            :return: an "aware" datetime object representing the last date/time the issue was updated
            """
            updated_at: str = self.git_issue.updated_at
            if updated_at[-1] in ('z', 'Z'):  # Didn't encounter this problem with updated_at
                updated_at = updated_at[:-1] + '+00:00'
            return datetime.fromisoformat(updated_at)
    
        def get_closed_at(self) -> Optional[datetime]:
            """
            :return: an "aware" datetime object representing the last date/time the issue was closed, or None if the issue
            is open
            """
            closed_at: str = self.git_issue.closed_at
            if closed_at is None:
                return None
            else:
                if closed_at[-1] in ('z', 'Z'):  # Did encounter this problem with closed_at
                    closed_at = closed_at[:-1] + '+00:00'
                return datetime.fromisoformat(closed_at)
    
        def get_labels(self) -> Set[str]:
            """
            :return: set of label names
            """
            return set(self.git_issue.labels)
            # return self.git_issue.labels.list(all=True)
    
        def get_page(self) -> str:
            """
            :return: HTTPS URL to issue's page
            """
            return self.git_issue.web_url
    
        def __repr__(self) -> str:
            issue_number = self.get_project_issue_id()
            title = self.get_title()
            return f'{issue_number}. {title}'
    
        # other git_issue fields:
        # project_id
        # title
        # description
        # closed_by             user
        # milestone
        # assignees             list of users
        # author                user
        # assignee              user
        # user_notes_count
        # merge_requests_count
        # upvotes
        # downvotes
        # due_date              same date format, or None
        # confidential
        # discussion_locked
        # time_stats
        # task_completion_status
        # has_tasks
        # _links
        # notes
        # award_emoji
        # project
        # subscribed
    
    
    class GitlabCommit:
        gitlab_commit: ProjectCommit
    
        # noinspection PyShadowingNames
        def __init__(self, commit: ProjectCommit):
            super().__init__()
            self.gitlab_commit = commit
    
        def get_author(self) -> Dict[str, str]:
            return {'name': self.gitlab_commit.author_name, 'email': self.gitlab_commit.author_email}
    
        def get_timestamp(self) -> str:  # TODO: is a string (vs a datetime) really what we want?
            return self.gitlab_commit.created_at
    
        def get_message(self) -> str:
            return self.gitlab_commit.message
    
        def is_merge(self) -> bool:
            return len(self.gitlab_commit.parent_ids) > 1
    
        # noinspection PyShadowingNames
        def get_diffs(self) -> List[Dict[str, Union[str, int]]]:
            diffs: List[Dict[str, Union[str, int]]] = []
            gitlab_diffs: List[Dict[str, str]] = self.gitlab_commit.diff()
            for diff in gitlab_diffs:
                diffs.append({'file': diff['new_path'], 'text': diff['diff'],
                              '+': diff['diff'].count('\n+'), '-': diff['diff'].count('\n-')})
            return diffs
    
        # noinspection PyShadowingNames
        def get_diff_size(self) -> int:
            insertions = 0
            deletions = 0
            if not self.is_merge():
                for diff in self.get_diffs():
                    insertions += diff['+']
                    deletions += diff['-']
            return max(insertions, deletions)
    
        # git_commit fields:
        # comments
        # discussions
        # manager
        # statuses
        # attributes:
        # id
        # short_id
        # created_at
        # parent_ids
        # title
        # message
        # author_name
        # author_email
        # authored_date
        # committer_name
        # committer_email
        # committed_date
        # project_id
    
    
    class GitlabProject:
        git_project: Project
    
        def __init__(self, project: Union[int, str, Project]):
            """
            Creates a Project object, populating the backing git_project instance with the appropriate gitlab.Project object
            :param project: must be either a gitlab.Project object, an integer representing the project ID, or a string
            containing the project's path (namespace and name, such as 'csce_361/sandbox/HelloWorld')
            """
            super().__init__()
            if isinstance(project, int):  # by project id
                self.git_project = GitlabSession.get_session().projects.get(project)
            elif isinstance(project, str):  # by path
                self.git_project = GitlabSession.get_session().projects.get(project)
            else:
                # self.git_project = project        # for some reason, many attributes (including members) might be lost
                self.git_project = GitlabSession.get_session().projects.get(project.id)
    
        @staticmethod
        def get_projects_by_group(group: Union[int, str]) -> List["GitlabProject"]:
            """
            :param group: must be either an integer representing the group ID, or a string containing the group's namespace
            :return: list of projects in the specified group
            """
            if isinstance(group, int):  # by group id
                gitlab_projects: Iterable[Project] = GitlabSession.get_session().groups.get(group).projects.list(all=True)
            else:  # isinstance(group, str):        # by path
                gitlab_projects = GitlabSession.get_session().groups.get(group).projects.list(all=True)
            projects: List[GitlabProject] = []
            for project in gitlab_projects:
                projects.append(GitlabProject(project))
            return projects
    
        @staticmethod
        def get_projects_by_keyword(search_term: str) -> List["GitlabProject"]:
            gitlab_projects: Iterable[Project] = GitlabSession.get_session().projects.list(search=search_term, all=True)
            projects: List[GitlabProject] = []
            for project in gitlab_projects:
                projects.append(GitlabProject(project))
            return projects
    
        @staticmethod
        def create_project(project_name: str) -> "GitlabProject":
            return GitlabProject(GitlabSession.get_session().projects.create({'name': project_name}))
    
        @staticmethod
        def create_project_in_group(group_name: str, project_name: str) -> "GitlabProject":
            group_id = GitlabSession.get_session().groups.get(group_name).id
            return GitlabProject(GitlabSession.get_session().projects.create({'name': project_name,
                                                                              'namespace_id': group_id}))
    
        def get_project_id(self) -> int:
            return self.git_project.id
    
        def get_name(self) -> str:
            """
            :return: project name without namespace
            """
            return self.git_project.name
    
        def get_name_with_namespace(self) -> str:
            """
            :return: project name with namespace, spaces around slashes
            """
            return self.git_project.name_with_namespace
    
        def get_path(self) -> str:
            """
            :return: path without namespace (may differ from name if name has spaces)
            """
            return self.git_project.path
    
        def get_path_with_namespace(self) -> str:
            """
            :return: path with namespace, no spaces around slashes
            """
            return self.git_project.path_with_namespace
    
        def get_cloning_url(self) -> str:
            """
            :return: SSH URL to clone repository
            """
            return self.git_project.ssh_url_to_repo
    
        def get_site(self) -> str:
            """
            :return: HTTPS URL to git site
            """
            return self.git_project.web_url
    
        def get_readme_url(self) -> str:
            """
            :return: HTTPS URL to README.md
            """
            return self.git_project.readme_url
    
        def get_visibility(self) -> str:
            """
            :return: 'private', etc.
            """
            return self.git_project.visibility
    
        def get_creator(self) -> GitlabUser:
            """
            :return: User object backed by the gitlab.User object representing the user who created the repo
            """
            return GitlabUser(self.git_project.creator_id)
    
        def get_created_at(self) -> datetime:
            """
            :return: an "aware" datetime object representing the creation date/time
            """
            created_at: str = self.git_project.created_at
            if created_at[-1] in ('z', 'Z'):
                created_at = created_at[:-1] + '+00:00'
            return datetime.fromisoformat(created_at)
    
        def get_users(self) -> List[GitlabUser]:
            """
            :return: List of User objects representing the project's members (not including inherited members)
            """
            gitlab_users: Iterable[User] = self.git_project.members.list(all=True)
            users: List[GitlabUser] = []
            for user in gitlab_users:
                users.append(GitlabUser(user))
            return users
    
        def get_all_users(self) -> List[GitlabUser]:
            """
            :return: List of User objects representing all of the project's members (including inherited members)
            """
            gitlab_users: Iterable[User] = self.git_project.members.all(all=True)
            users: List[GitlabUser] = []
            for user in gitlab_users:
                users.append(GitlabUser(user))
            return users
    
        def add_user_as_maintainer(self, user: GitlabUser) -> None:
            self.git_project.members.create({'user_id': user.get_user_id(), 'access_level': MAINTAINER_ACCESS})
    
        def get_issues(self) -> List[GitlabIssue]:
            """
            :return: List of Issue objects representing project's issues, sorted by creation date
            """
            gitlab_issues: Iterable[Issue] = self.git_project.issues.list(order_by='created_at', sort='asc', all=True)
            issues: List[GitlabIssue] = []
            for issue in gitlab_issues:
                issues.append(GitlabIssue(issue))
            return issues
    
        def create_issue(self, title: str, description: str) -> GitlabIssue:
            gitlab_issue: Issue = self.git_project.issues.create({'title': title, 'description': description})
            return GitlabIssue(gitlab_issue)
    
        # noinspection PyShadowingNames
        def get_commits(self, branch_name: str = '', after_date: str = '1970-01-01', before_date: str = '9999-12-31') -> \
                List[GitlabCommit]:
            """
            :param branch_name: the branch to retrieve commits from; if an empty string (default) then retrieves commits
            from all branches <-- NO, RETRIEVES FROM DEFAULT BRANCH; WILL NEED TO FIX THAT #TODO
            :param after_date: the earliest date of any retrieved commit; if '1970-01-01' (Unix epoch) then treated as
            having no earliest-bound
            :param before_date: the latest date of any retrieved commit; if '9999-12-31' (Y10K problem) then treated a
            having no latest-bound
            :return: List of Commit objects representing the project's commits that meet the specified constraints
            """
            filters: Dict[str, str] = {}
            if branch_name != '':
                filters['ref_name'] = branch_name
            if after_date != '1970-01-01':
                filters['since'] = after_date
            if before_date != '9999-12-31':
                filters['until'] = before_date
            if len(filters) == 0:
                gitlab_commits: Iterable[ProjectCommit] = self.git_project.commits.list(all=True)
            else:
                gitlab_commits = self.git_project.commits.list(all=True, query_parameters=filters)
            commits: List[GitlabCommit] = []
            for commit in gitlab_commits:
                commits.append(GitlabCommit(commit))
            return commits
    
        def get_labels(self) -> Set[str]:
            """
            :return: set of label names
            """
            return set(map(lambda label: label.name, self.git_project.labels.list()))
    
        def __repr__(self) -> str:
            return self.get_name_with_namespace()
    
        # other git_project fields:
        # description
        # default_branch
        # tag_list
        # http_url_to_repo      https URL to clone repository
        # avatar_url
        # star_count
        # forks_count
        # last_activity_at
        # namespace             namespace's JSON object
        # _links                JSON object with api/v4 links to self, issues, merge_requests,
        #                                                        repo_branches, labels, events, members
        # empty_repo
        # archived
        # resolve_outdated_diff_discussions
        # container_registry_enabled
        # issues_enabled
        # merge_requests_enabled
        # jobs_enabled
        # snippets_enabled
        # issues_access_level
        # repository_access_level,
        # wiki_access_level
        # builds_access_level
        # snippets_access_level
        # shared_runners_enabled
        # lfs_enabled
        # import_status
        # import_error
        # open_issues_count
        # runners_token
        # ci_default_git_depth
        # public_jobs
        # build_git_strategy
        # build_timeout
        # auto_cancel_pending_pipelines
        # build_coverage_regex
        # ci_config_path
        # shared_with_groups
        # only_allow_merge_if_pipeline_succeeds
        # request_access_enabled
        # only_allow_merge_if_all_discussions_are_resolved
        # printing_merge_request_link_enabled
        # merge_method
        # auto_devops_enabled
        # auto_devops_deploy_strategy
        # permissions
    
    
    if __name__ == '__main__':
        namespace = 'csce_361/sandbox'
        test_projects = GitlabProject.get_projects_by_group(namespace)
        print('All projects in sandbox:')
        for test_project in test_projects:
            print(test_project)
        print('Selecting last project. Here are the commits:')
        test_project = test_projects[-1]
        commits = test_project.get_commits()
        diff = commits[-5].get_diffs()
        print(diff)
        print(commits[0].get_author())
        for commit in commits:
            print(commit.get_message())
            print(f'is a merge? {commit.is_merge()}')
            print(f'size: {commit.get_diff_size()}')
            print()
        """
        print('Selecting second project. Here are the members:')
        test_project = test_projects[1]
        members = test_project.get_users()
        for member in members:
            print(member)
        print('Here are ALL the members:')
        members = test_project.get_all_users()
        for member in members:
            print(member)
        print('Here are the issues:')
        test_issues = test_project.get_issues()
        for test_issue in test_issues:
            creation = test_issue.get_created_at()
            print(f'{test_issue}\tcreated at {creation}.')
        test_projects = GitlabProject.get_projects_by_keyword('csce361-homework')
        number_of_projects = len(test_projects)
        print(f'retrieved {number_of_projects} projects matching \'csce361-homework\'')
        start_date = datetime(2019, 8, 1, tzinfo=timezone.utc)
        test_projects = list(filter(lambda p: p.get_created_at() > start_date, test_projects))
        new_number_of_projects = len(test_projects)
        print(f'after culling, there are {new_number_of_projects} projects that were created in/after August 2019')
        print(f'including {test_projects[0]} created by {test_projects[0].get_creator()} at '
              f'{test_projects[0].get_created_at()}.')
        """