From f3cd5884e5ac8eeafa667c499dd422c4eb05f729 Mon Sep 17 00:00:00 2001 From: Packit Date: Fri, 22 Dec 2023 11:05:54 +0000 Subject: [PATCH] Update docs for fbf35d3 --- .nojekyll | 0 docs/abstract.html | 8651 +++++++++++++++++ docs/constant.html | 63 + docs/deprecation.html | 126 + docs/exceptions.html | 537 + docs/factory.html | 685 ++ docs/index.html | 1863 ++++ docs/parsing.html | 796 ++ docs/read_only.html | 925 ++ docs/services/base.html | 663 ++ .../github/auth_providers/abstract.html | 252 + .../github/auth_providers/github_app.html | 367 + .../services/github/auth_providers/index.html | 557 ++ .../services/github/auth_providers/token.html | 165 + .../github/auth_providers/tokman.html | 182 + docs/services/github/check_run.html | 1483 +++ docs/services/github/comments.html | 316 + docs/services/github/flag.html | 238 + docs/services/github/index.html | 3141 ++++++ docs/services/github/issue.html | 479 + docs/services/github/project.html | 1483 +++ docs/services/github/pull_request.html | 687 ++ docs/services/github/release.html | 388 + docs/services/github/service.html | 665 ++ docs/services/github/user.html | 224 + docs/services/gitlab/comments.html | 364 + docs/services/gitlab/flag.html | 294 + docs/services/gitlab/index.html | 2072 ++++ docs/services/gitlab/issue.html | 493 + docs/services/gitlab/project.html | 1339 +++ docs/services/gitlab/pull_request.html | 794 ++ docs/services/gitlab/release.html | 283 + docs/services/gitlab/service.html | 491 + docs/services/gitlab/user.html | 164 + docs/services/index.html | 87 + docs/services/pagure/comments.html | 251 + docs/services/pagure/flag.html | 266 + docs/services/pagure/index.html | 2847 ++++++ docs/services/pagure/issue.html | 617 ++ docs/services/pagure/project.html | 1422 +++ docs/services/pagure/pull_request.html | 1042 ++ docs/services/pagure/release.html | 285 + docs/services/pagure/service.html | 1187 +++ docs/services/pagure/user.html | 211 + docs/utils.html | 642 ++ 45 files changed, 40087 insertions(+) create mode 100644 .nojekyll create mode 100644 docs/abstract.html create mode 100644 docs/constant.html create mode 100644 docs/deprecation.html create mode 100644 docs/exceptions.html create mode 100644 docs/factory.html create mode 100644 docs/index.html create mode 100644 docs/parsing.html create mode 100644 docs/read_only.html create mode 100644 docs/services/base.html create mode 100644 docs/services/github/auth_providers/abstract.html create mode 100644 docs/services/github/auth_providers/github_app.html create mode 100644 docs/services/github/auth_providers/index.html create mode 100644 docs/services/github/auth_providers/token.html create mode 100644 docs/services/github/auth_providers/tokman.html create mode 100644 docs/services/github/check_run.html create mode 100644 docs/services/github/comments.html create mode 100644 docs/services/github/flag.html create mode 100644 docs/services/github/index.html create mode 100644 docs/services/github/issue.html create mode 100644 docs/services/github/project.html create mode 100644 docs/services/github/pull_request.html create mode 100644 docs/services/github/release.html create mode 100644 docs/services/github/service.html create mode 100644 docs/services/github/user.html create mode 100644 docs/services/gitlab/comments.html create mode 100644 docs/services/gitlab/flag.html create mode 100644 docs/services/gitlab/index.html create mode 100644 docs/services/gitlab/issue.html create mode 100644 docs/services/gitlab/project.html create mode 100644 docs/services/gitlab/pull_request.html create mode 100644 docs/services/gitlab/release.html create mode 100644 docs/services/gitlab/service.html create mode 100644 docs/services/gitlab/user.html create mode 100644 docs/services/index.html create mode 100644 docs/services/pagure/comments.html create mode 100644 docs/services/pagure/flag.html create mode 100644 docs/services/pagure/index.html create mode 100644 docs/services/pagure/issue.html create mode 100644 docs/services/pagure/project.html create mode 100644 docs/services/pagure/pull_request.html create mode 100644 docs/services/pagure/release.html create mode 100644 docs/services/pagure/service.html create mode 100644 docs/services/pagure/user.html create mode 100644 docs/utils.html diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/docs/abstract.html b/docs/abstract.html new file mode 100644 index 00000000..d4465b12 --- /dev/null +++ b/docs/abstract.html @@ -0,0 +1,8651 @@ + + + + + + +ogr.abstract API documentation + + + + + + + + + + + +
+
+
+

Module ogr.abstract

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+import functools
+from collections.abc import Sequence
+from enum import Enum, IntEnum
+from re import Match
+from typing import (
+    Any,
+    Callable,
+    ClassVar,
+    Optional,
+    TypeVar,
+    Union,
+)
+
+import github
+import gitlab
+import requests
+
+from ogr.deprecation import deprecate_and_set_removal
+from ogr.exceptions import (
+    APIException,
+    GitForgeInternalError,
+    GithubAPIException,
+    GitlabAPIException,
+    OgrException,
+    OgrNetworkError,
+)
+from ogr.parsing import parse_git_repo
+
+try:
+    from functools import cached_property as _cached_property
+except ImportError:
+    from functools import lru_cache
+
+    def _cached_property(func):  # type: ignore
+        return property(lru_cache()(func))
+
+
+AnyComment = TypeVar("AnyComment", bound="Comment")
+
+
+def __check_for_internal_failure(ex: APIException):
+    """
+    Checks if exception is caused by internal failure from git forge.
+
+    Args:
+        ex: Wrapped exception.
+
+    Raises:
+        GitForgeInternalError, when exception was cause by an internal failure.
+        APIException, exception itself when not an internal failure.
+    """
+    if ex.response_code is not None and ex.response_code >= 500:
+        raise GitForgeInternalError from ex.__cause__
+    raise ex
+
+
+def __wrap_exception(
+    ex: Union[github.GithubException, gitlab.GitlabError],
+) -> APIException:
+    """
+    Wraps uncaught exception in one of ogr exceptions.
+
+    Args:
+        ex: Unhandled exception from GitHub or GitLab.
+
+    Returns:
+        Wrapped `ex` in respective `APIException`.
+
+    Raises:
+        TypeError, when given unexpected type of exception.
+    """
+    MAPPING = {
+        github.GithubException: GithubAPIException,
+        gitlab.GitlabError: GitlabAPIException,
+    }
+
+    for caught_exception, ogr_exception in MAPPING.items():
+        if isinstance(ex, caught_exception):
+            exc = ogr_exception()
+            exc.__cause__ = ex
+            return exc
+
+    raise TypeError("Unknown type of uncaught exception passed") from ex
+
+
+def catch_common_exceptions(function: Callable) -> Any:
+    """
+    Decorator catching common exceptions.
+
+    Args:
+        function (Callable): Function or method to decorate.
+
+    Raises:
+        GithubAPIException, if authentication to Github failed.
+        GitlabAPIException, if authentication to Gitlab failed.
+        OgrNetworkError, if network problems occurred while performing a request.
+    """
+
+    @functools.wraps(function)
+    def wrapper(*args, **kwargs):
+        try:
+            return function(*args, **kwargs)
+        except github.BadCredentialsException as ex:
+            raise GithubAPIException("Invalid Github credentials") from ex
+        except gitlab.GitlabAuthenticationError as ex:
+            raise GitlabAPIException("Invalid Gitlab credentials") from ex
+        except requests.exceptions.ConnectionError as ex:
+            raise OgrNetworkError(
+                "Could not perform the request due to a network error",
+            ) from ex
+        except APIException as ex:
+            __check_for_internal_failure(ex)
+        except (github.GithubException, gitlab.GitlabError) as ex:
+            __check_for_internal_failure(__wrap_exception(ex))
+
+    return wrapper
+
+
+class CatchCommonErrors(type):
+    """
+    A metaclass wrapping methods with a common exception handler.
+
+    This handler catches exceptions which can occur almost anywhere
+    and catching them manually would be tedious and converts them
+    to an appropriate ogr exception for the user. This includes
+    exceptions such as:
+        - authentication (from Github/Gitlab)
+        - network errors
+    """
+
+    def __new__(cls, name, bases, namespace):
+        for key, value in namespace.items():
+            # There is an anticipated change in behaviour in Python 3.10
+            # for static/class methods. From Python 3.10 they will be callable.
+            # We need to achieve consistent behaviour with older versions,
+            # hence the explicit handling is needed here (isinstance checking
+            # works the same). Moreover, static/class method decorator must
+            # be used last, especially prior to Python 3.10 since they return
+            # descriptor objects and not functions.
+            # See: https://bugs.python.org/issue43682
+            if isinstance(value, staticmethod):
+                namespace[key] = staticmethod(catch_common_exceptions(value.__func__))
+            elif isinstance(value, classmethod):
+                namespace[key] = classmethod(catch_common_exceptions(value.__func__))
+            elif callable(namespace[key]):
+                namespace[key] = catch_common_exceptions(namespace[key])
+        return super().__new__(cls, name, bases, namespace)
+
+
+class OgrAbstractClass(metaclass=CatchCommonErrors):
+    def __repr__(self) -> str:
+        return f"<{self!s}>"
+
+
+class Reaction(OgrAbstractClass):
+    def __init__(self, raw_reaction: Any) -> None:
+        self._raw_reaction = raw_reaction
+
+    def __str__(self):
+        return f"Reaction(raw_reaction={self._raw_reaction})"
+
+    def delete(self) -> None:
+        """Delete a reaction."""
+        raise NotImplementedError()
+
+
+class Comment(OgrAbstractClass):
+    def __init__(
+        self,
+        raw_comment: Optional[Any] = None,
+        parent: Optional[Any] = None,
+        body: Optional[str] = None,
+        id_: Optional[int] = None,
+        author: Optional[str] = None,
+        created: Optional[datetime.datetime] = None,
+        edited: Optional[datetime.datetime] = None,
+    ) -> None:
+        if raw_comment:
+            self._from_raw_comment(raw_comment)
+        elif body and author:
+            self._body = body
+            self._id = id_
+            self._author = author
+            self._created = created
+            self._edited = edited
+        else:
+            raise ValueError("cannot construct comment without body and author")
+
+        self._parent = parent
+
+    def __str__(self) -> str:
+        body = f"{self.body[:10]}..." if self.body is not None else "None"
+        return (
+            f"Comment("
+            f"comment='{body}', "
+            f"author='{self.author}', "
+            f"created='{self.created}', "
+            f"edited='{self.edited}')"
+        )
+
+    def _from_raw_comment(self, raw_comment: Any) -> None:
+        """Constructs Comment object from raw_comment given from API."""
+        raise NotImplementedError()
+
+    @property
+    def body(self) -> str:
+        """Body of the comment."""
+        return self._body
+
+    @body.setter
+    def body(self, new_body: str) -> None:
+        self._body = new_body
+
+    @property
+    def id(self) -> int:
+        return self._id
+
+    @property
+    def author(self) -> str:
+        """Login of the author of the comment."""
+        return self._author
+
+    @property
+    def created(self) -> datetime.datetime:
+        """Datetime of creation of the comment."""
+        return self._created
+
+    @property
+    def edited(self) -> datetime.datetime:
+        """Datetime of last edit of the comment."""
+        return self._edited
+
+    def get_reactions(self) -> list[Reaction]:
+        """Returns list of reactions."""
+        raise NotImplementedError()
+
+    def add_reaction(self, reaction: str) -> Reaction:
+        """
+        Reacts to a comment.
+
+        Colons in between reaction are not needed, e.g. `comment.add_reaction("+1")`.
+
+        Args:
+            reaction: String representing specific reaction to be added.
+
+        Returns:
+            Object representing newly added reaction.
+        """
+        raise NotImplementedError()
+
+
+class IssueComment(Comment):
+    @property
+    def issue(self) -> "Issue":
+        """Issue of issue comment."""
+        return self._parent
+
+    def __str__(self) -> str:
+        return "Issue" + super().__str__()
+
+
+class PRComment(Comment):
+    @property
+    def pull_request(self) -> "PullRequest":
+        """Pull request of pull request comment."""
+        return self._parent
+
+    def __str__(self) -> str:
+        return "PR" + super().__str__()
+
+
+class IssueStatus(IntEnum):
+    """Enumeration for issue statuses."""
+
+    open = 1
+    closed = 2
+    all = 3
+
+
+class Issue(OgrAbstractClass):
+    """
+    Attributes:
+        project (GitProject): Project of the issue.
+    """
+
+    def __init__(self, raw_issue: Any, project: "GitProject") -> None:
+        self._raw_issue = raw_issue
+        self.project = project
+
+    @property
+    def title(self) -> str:
+        """Title of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def private(self) -> bool:
+        """`True` if issue is confidential, `False` otherwise."""
+        raise NotImplementedError()
+
+    @property
+    def id(self) -> int:
+        """ID of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def status(self) -> IssueStatus:
+        """Status of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def url(self) -> str:
+        """Web URL of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def description(self) -> str:
+        """Description of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def author(self) -> str:
+        """Username of the author of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def created(self) -> datetime.datetime:
+        """Datetime of the creation of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def labels(self) -> list:
+        """Labels of the issue."""
+        raise NotImplementedError()
+
+    def __str__(self) -> str:
+        description = (
+            f"{self.description[:10]}..." if self.description is not None else "None"
+        )
+        return (
+            f"Issue("
+            f"title='{self.title}', "
+            f"id={self.id}, "
+            f"status='{self.status.name}', "
+            f"url='{self.url}', "
+            f"description='{description}', "
+            f"author='{self.author}', "
+            f"created='{self.created}')"
+        )
+
+    @staticmethod
+    def create(
+        project: Any,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> "Issue":
+        """
+        Open new issue.
+
+        Args:
+            project (GitProject): Project where the issue is to be opened.
+            title: Title of the issue.
+            body: Description of the issue.
+            private: Is the new issue supposed to be confidential?
+
+                **Supported only by GitLab and Pagure.**
+
+                Defaults to unset.
+            labels: List of labels that are to be added to
+                the issue.
+
+                Defaults to no labels.
+            assignees: List of usernames of the assignees.
+
+                Defaults to no assignees.
+
+        Returns:
+            Object that represents newly created issue.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get(project: Any, id: int) -> "Issue":
+        """
+        Get issue.
+
+        Args:
+            project (GitProject): Project where the issue is to be opened.
+            issue_id: ID of the issue.
+
+        Returns:
+            Object that represents requested issue.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get_list(
+        project: Any,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        """
+        List of issues.
+
+        Args:
+            project (GitProject): Project where the issue is to be opened.
+            status: Status of the issues that are to be
+                included in the list.
+
+                Defaults to `IssueStatus.open`.
+            author: Username of the author of the issues.
+
+                Defaults to no filtering by author.
+            assignee: Username of the assignee on the issues.
+
+                Defaults to no filtering by assignees.
+            labels: Filter issues that have set specific labels.
+
+                Defaults to no filtering by labels.
+
+        Returns:
+            List of objects that represent requested issues.
+        """
+        raise NotImplementedError()
+
+    def _get_all_comments(self) -> list[IssueComment]:
+        """
+        Get list of all issue comments.
+
+        Returns:
+            List of all comments on the issue.
+        """
+        raise NotImplementedError()
+
+    def get_comments(
+        self,
+        filter_regex: Optional[str] = None,
+        reverse: bool = False,
+        author: Optional[str] = None,
+    ) -> list[IssueComment]:
+        """
+        Get list of issue comments.
+
+        Args:
+            filter_regex: Filter the comments' content with `re.search`.
+
+                Defaults to `None`, which means no filtering.
+            reverse: Whether the comments are to be returned in
+                reversed order.
+
+                Defaults to `False`.
+            author: Filter the comments by author.
+
+                Defaults to `None`, which means no filtering.
+
+        Returns:
+            List of issue comments.
+        """
+        raise NotImplementedError()
+
+    def can_close(self, username: str) -> bool:
+        """
+        Check if user have permissions to modify an issue.
+
+        Args:
+            username: Login of the user.
+
+        Returns:
+            `True` if user can close the issue, `False` otherwise.
+        """
+        raise NotImplementedError()
+
+    def comment(self, body: str) -> IssueComment:
+        """
+        Add new comment to the issue.
+
+        Args:
+            body: Text contents of the comment.
+
+        Returns:
+            Object that represents posted comment.
+        """
+        raise NotImplementedError()
+
+    def close(self) -> "Issue":
+        """
+        Close an issue.
+
+        Returns:
+            Issue itself.
+        """
+        raise NotImplementedError()
+
+    def add_label(self, *labels: str) -> None:
+        """
+        Add labels to the issue.
+
+        Args:
+            *labels: Labels to be added.
+        """
+        raise NotImplementedError()
+
+    def add_assignee(self, *assignees: str) -> None:
+        """
+        Assign users to an issue.
+
+        Args:
+            *assignees: List of logins of the assignees.
+        """
+        raise NotImplementedError()
+
+    def get_comment(self, comment_id: int) -> IssueComment:
+        """
+        Returns an issue comment.
+
+        Args:
+            comment_id: id of a comment
+
+        Returns:
+            Object representing an issue comment.
+        """
+        raise NotImplementedError()
+
+
+class PRStatus(IntEnum):
+    """Enumeration that represents statuses of pull requests."""
+
+    open = 1
+    closed = 2
+    merged = 3
+    all = 4
+
+
+class CommitStatus(Enum):
+    """Enumeration that represents possible state of commit statuses."""
+
+    pending = 1
+    success = 2
+    failure = 3
+    error = 4
+    canceled = 5
+    running = 6
+
+
+class MergeCommitStatus(Enum):
+    """Enumeration that represents possible states of merge states of PR/MR."""
+
+    can_be_merged = 1
+    cannot_be_merged = 2
+    unchecked = 3
+    checking = 4
+    cannot_be_merged_recheck = 5
+
+
+class PullRequest(OgrAbstractClass):
+    """
+    Attributes:
+        project (GitProject): Project of the pull request.
+    """
+
+    def __init__(self, raw_pr: Any, project: "GitProject") -> None:
+        self._raw_pr = raw_pr
+        self._target_project = project
+
+    @property
+    def title(self) -> str:
+        """Title of the pull request."""
+        raise NotImplementedError()
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        raise NotImplementedError()
+
+    @property
+    def id(self) -> int:
+        """ID of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def status(self) -> PRStatus:
+        """Status of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def url(self) -> str:
+        """Web URL of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def description(self) -> str:
+        """Description of the pull request."""
+        raise NotImplementedError()
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        raise NotImplementedError
+
+    @property
+    def author(self) -> str:
+        """Login of the author of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def source_branch(self) -> str:
+        """Name of the source branch (from which the changes are pulled)."""
+        raise NotImplementedError()
+
+    @property
+    def target_branch(self) -> str:
+        """Name of the target branch (where the changes are being merged)."""
+        raise NotImplementedError()
+
+    @property
+    def created(self) -> datetime.datetime:
+        """Datetime of creating the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def labels(self) -> list[Any]:
+        """Labels of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def diff_url(self) -> str:
+        """Web URL to the diff of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def patch(self) -> bytes:
+        """Patch of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def head_commit(self) -> str:
+        """Commit hash of the HEAD commit of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def target_branch_head_commit(self) -> str:
+        """Commit hash of the HEAD commit of the target branch."""
+        raise NotImplementedError()
+
+    @property
+    def merge_commit_sha(self) -> str:
+        """
+        Commit hash of the merge commit of the pull request.
+
+        Before merging represents test merge commit, if git forge supports it.
+        """
+        raise NotImplementedError()
+
+    @property
+    def merge_commit_status(self) -> MergeCommitStatus:
+        """Current status of the test merge commit."""
+        raise NotImplementedError()
+
+    @property
+    def source_project(self) -> "GitProject":
+        """Object that represents source project (from which the changes are pulled)."""
+        raise NotImplementedError()
+
+    @property
+    def target_project(self) -> "GitProject":
+        """Object that represents target project (where changes are merged)."""
+        return self._target_project
+
+    @property
+    def commits_url(self) -> str:
+        """Web URL to the list of commits in the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def closed_by(self) -> Optional[str]:
+        """Login of the account that closed the pull request."""
+        raise NotImplementedError
+
+    def __str__(self) -> str:
+        description = (
+            f"{self.description[:10]}..." if self.description is not None else "None"
+        )
+        return (
+            f"PullRequest("
+            f"title='{self.title}', "
+            f"id={self.id}, "
+            f"status='{self.status.name}', "
+            f"url='{self.url}', "
+            f"diff_url='{self.diff_url}', "
+            f"description='{description}', "
+            f"author='{self.author}', "
+            f"source_branch='{self.source_branch}', "
+            f"target_branch='{self.target_branch}', "
+            f"created='{self.created}')"
+        )
+
+    @staticmethod
+    def create(
+        project: Any,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        Create new pull request.
+
+        Args:
+            project (GitProject): Project where the pull request will be created.
+            title: Title of the pull request.
+            body: Description of the pull request.
+            target_branch: Branch in the project where the changes are being
+                merged.
+            source_branch: Branch from which the changes are being pulled.
+            fork_username: The username/namespace of the forked repository.
+
+        Returns:
+            Object that represents newly created pull request.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get(project: Any, id: int) -> "PullRequest":
+        """
+        Get pull request.
+
+        Args:
+            project (GitProject): Project where the pull request is located.
+            id: ID of the pull request.
+
+        Returns:
+            Object that represents pull request.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get_list(project: Any, status: PRStatus = PRStatus.open) -> list["PullRequest"]:
+        """
+        List of pull requests.
+
+        Args:
+            project (GitProject): Project where the pull requests are located.
+            status: Filters out the pull requests.
+
+                Defaults to `PRStatus.open`.
+
+        Returns:
+            List of pull requests with requested status.
+        """
+        raise NotImplementedError()
+
+    def update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        Update pull request information.
+
+        Args:
+            title: The new title of the pull request.
+
+                Defaults to `None`, which means no updating.
+            description: The new description of the pull request.
+
+                Defaults to `None`, which means no updating.
+
+        Returns:
+            Pull request itself.
+        """
+        raise NotImplementedError()
+
+    def _get_all_comments(self) -> list[PRComment]:
+        """
+        Get list of all pull request comments.
+
+        Returns:
+            List of all comments on the pull request.
+        """
+        raise NotImplementedError()
+
+    def get_comments(
+        self,
+        filter_regex: Optional[str] = None,
+        reverse: bool = False,
+        author: Optional[str] = None,
+    ) -> list["PRComment"]:
+        """
+        Get list of pull request comments.
+
+        Args:
+            filter_regex: Filter the comments' content with `re.search`.
+
+                Defaults to `None`, which means no filtering.
+            reverse: Whether the comments are to be returned in
+                reversed order.
+
+                Defaults to `False`.
+            author: Filter the comments by author.
+
+                Defaults to `None`, which means no filtering.
+
+        Returns:
+            List of pull request comments.
+        """
+        raise NotImplementedError()
+
+    def get_all_commits(self) -> list[str]:
+        """
+        Returns:
+            List of commit hashes of commits in pull request.
+        """
+        raise NotImplementedError()
+
+    def search(
+        self,
+        filter_regex: str,
+        reverse: bool = False,
+        description: bool = True,
+    ) -> Optional[Match[str]]:
+        """
+        Find match in pull request description or comments.
+
+        Args:
+            filter_regex: Regex that is used to filter the comments' content with `re.search`.
+            reverse: Reverse order of the comments.
+
+                Defaults to `False`.
+            description: Whether description is included in the search.
+
+                Defaults to `True`.
+
+        Returns:
+            `re.Match` if found, `None` otherwise.
+        """
+        raise NotImplementedError()
+
+    def comment(
+        self,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        """
+        Add new comment to the pull request.
+
+        Args:
+            body: Body of the comment.
+            commit: Commit hash to which comment is related.
+
+                Defaults to generic comment.
+            filename: Path to the file to which comment is related.
+
+                Defaults to no relation to the file.
+            row: Line number to which the comment is related.
+
+                Defaults to no relation to the line.
+
+        Returns:
+            Newly created comment.
+        """
+        raise NotImplementedError()
+
+    def close(self) -> "PullRequest":
+        """
+        Close the pull request.
+
+        Returns:
+            Pull request itself.
+        """
+        raise NotImplementedError()
+
+    def merge(self) -> "PullRequest":
+        """
+        Merge the pull request.
+
+        Returns:
+            Pull request itself.
+        """
+        raise NotImplementedError()
+
+    def add_label(self, *labels: str) -> None:
+        """
+        Add labels to the pull request.
+
+        Args:
+            *labels: Labels to be added.
+        """
+        raise NotImplementedError()
+
+    def get_statuses(self) -> list["CommitFlag"]:
+        """
+        Returns statuses for latest commit on pull request.
+
+        Returns:
+            List of commit statuses of the latest commit.
+        """
+        raise NotImplementedError()
+
+    def get_comment(self, comment_id: int) -> PRComment:
+        """
+        Returns a PR comment.
+
+        Args:
+            comment_id: id of comment
+
+        Returns:
+            Object representing a PR comment.
+        """
+        raise NotImplementedError()
+
+
+class CommitFlag(OgrAbstractClass):
+    _states: ClassVar[dict[str, CommitStatus]] = {}
+
+    def __init__(
+        self,
+        raw_commit_flag: Optional[Any] = None,
+        project: Optional["GitProject"] = None,
+        commit: Optional[str] = None,
+        state: Optional[CommitStatus] = None,
+        context: Optional[str] = None,
+        comment: Optional[str] = None,
+        uid: Optional[str] = None,
+        url: Optional[str] = None,
+    ) -> None:
+        self.uid = uid
+        self.project = project
+        self.commit = commit
+
+        if commit and state and context:
+            self.state = state
+            self.context = context
+            self.comment = comment
+            self.url = url
+        else:
+            self._raw_commit_flag = raw_commit_flag
+            self._from_raw_commit_flag()
+
+    def __str__(self) -> str:
+        return (
+            f"CommitFlag("
+            f"commit='{self.commit}', "
+            f"state='{self.state.name}', "
+            f"context='{self.context}', "
+            f"uid='{self.uid}', "
+            f"comment='{self.comment}', "
+            f"url='{self.url}', "
+            f"created='{self.created}', "
+            f"edited='{self.edited}')"
+        )
+
+    @classmethod
+    def _state_from_str(cls, state: str) -> CommitStatus:
+        """
+        Transforms state from string to enumeration.
+
+        Args:
+            state: String representation of a state.
+
+        Returns:
+            Commit status.
+        """
+        raise NotImplementedError()
+
+    @classmethod
+    def _validate_state(cls, state: CommitStatus) -> CommitStatus:
+        """
+        Validates state of the commit status (if it can be used with forge).
+        """
+        raise NotImplementedError()
+
+    def _from_raw_commit_flag(self) -> None:
+        """
+        Sets attributes based on the raw flag that has been given through constructor.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get(project: Any, commit: str) -> list["CommitFlag"]:
+        """
+        Acquire commit statuses for given commit in the project.
+
+        Args:
+            project (GitProject): Project where the commit is located.
+            commit: Commit hash for which we request statuses.
+
+        Returns:
+            List of commit statuses for the commit.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def set(
+        project: Any,
+        commit: str,
+        state: CommitStatus,
+        target_url: str,
+        description: str,
+        context: str,
+    ) -> "CommitFlag":
+        """
+        Set a new commit status.
+
+        Args:
+            project (GitProject): Project where the commit is located.
+            commit: Commit hash for which we set status.
+            state: State for the commit status.
+            target_url: URL for the commit status.
+            description: Description of the commit status.
+            context: Identifier to group related commit statuses.
+        """
+        raise NotImplementedError()
+
+    @property
+    def created(self) -> datetime.datetime:
+        """Datetime of creating the commit status."""
+        raise NotImplementedError()
+
+    @property
+    def edited(self) -> datetime.datetime:
+        """Datetime of editing the commit status."""
+        raise NotImplementedError()
+
+
+class CommitComment(OgrAbstractClass):
+    """
+    Attributes:
+        sha (str): Hash of the related commit.
+        body (str): Body of the comment.
+        author (str): Login of the author.
+    """
+
+    def __init__(self, sha: str, body: str, author: str) -> None:
+        self.sha = sha
+        self.body = body
+        self.author = author
+
+    @property  # type: ignore
+    @deprecate_and_set_removal(
+        since="0.41.0",
+        remove_in="0.46.0 (or 1.0.0 if it comes sooner)",
+        message="Use body",
+    )
+    def comment(self) -> str:
+        return self.body
+
+    def __str__(self) -> str:
+        return (
+            f"CommitComment(commit={self.sha}, author={self.author}, body={self.body})"
+        )
+
+
+class GitTag(OgrAbstractClass):
+    """
+    Class representing a git tag.
+
+    Attributes:
+        name (str): Name of the tag.
+        commit_sha (str): Commit hash of the tag.
+    """
+
+    def __init__(self, name: str, commit_sha: str) -> None:
+        self.name = name
+        self.commit_sha = commit_sha
+
+    def __str__(self) -> str:
+        return f"GitTag(name={self.name}, commit_sha={self.commit_sha})"
+
+
+class AccessLevel(IntEnum):
+    """
+    Enumeration representing an access level to the repository.
+
+    | Value from enumeration | GitHub   | GitLab                  | Pagure |
+    | ---------------------- | -------- | ----------------------- | ------ |
+    | `AccessLevel.pull`     | pull     | guest                   | ticket |
+    | `AccessLevel.triage`   | triage   | reporter                | ticket |
+    | `AccessLevel.push`     | push     | developer               | commit |
+    | `AccessLevel.admin`    | admin    | maintainer              | commit |
+    | `AccessLevel.maintain` | maintain | owner (only for groups) | admin  |
+    """
+
+    pull = 1
+    triage = 2
+    push = 3
+    admin = 4
+    maintain = 5
+
+
+class Release(OgrAbstractClass):
+    """
+    Object that represents release.
+
+    Attributes:
+        project (GitProject): Project on which the release is created.
+    """
+
+    def __init__(
+        self,
+        raw_release: Any,
+        project: "GitProject",
+    ) -> None:
+        self._raw_release = raw_release
+        self.project = project
+
+    def __str__(self) -> str:
+        return (
+            f"Release("
+            f"title='{self.title}', "
+            f"body='{self.body}', "
+            f"tag_name='{self.tag_name}', "
+            f"url='{self.url}', "
+            f"created_at='{self.created_at}', "
+            f"tarball_url='{self.tarball_url}')"
+        )
+
+    @property
+    def title(self) -> str:
+        """Title of the release."""
+        raise NotImplementedError()
+
+    @property
+    def body(self) -> str:
+        """Body of the release."""
+        raise NotImplementedError()
+
+    @property
+    def git_tag(self) -> GitTag:
+        """Object that represents tag tied to the release."""
+        raise NotImplementedError()
+
+    @property
+    def tag_name(self) -> str:
+        """Tag tied to the release."""
+        raise NotImplementedError()
+
+    @property
+    def url(self) -> Optional[str]:
+        """URL of the release."""
+        raise NotImplementedError()
+
+    # TODO: Check if should really be string
+    @property
+    def created_at(self) -> datetime.datetime:
+        """Datetime of creating the release."""
+        raise NotImplementedError()
+
+    @property
+    def tarball_url(self) -> str:
+        """URL of the tarball."""
+        raise NotImplementedError()
+
+    @staticmethod
+    def get(
+        project: Any,
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> "Release":
+        """
+        Get a single release.
+
+        Args:
+            identifier: Identifier of the release.
+
+                Defaults to `None`, which means not being used.
+            name: Name of the release.
+
+                Defaults to `None`, which means not being used.
+            tag_name: Tag that the release is tied to.
+
+                Defaults to `None`, which means not being used.
+
+        Returns:
+            Object that represents release that satisfies requested condition.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get_latest(project: Any) -> Optional["Release"]:
+        """
+        Returns:
+            Object that represents the latest release.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get_list(project: Any) -> list["Release"]:
+        """
+        Returns:
+            List of the objects that represent releases.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def create(
+        project: Any,
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> "Release":
+        """
+        Create new release.
+
+        Args:
+            project: Project where the release is to be created.
+            tag: Tag which is the release based off.
+            name: Name of the release.
+            message: Message or description of the release.
+            ref: Git reference, mainly commit hash for the release. If provided
+                git tag is created prior to creating a release.
+
+                Defaults to `None`.
+
+        Returns:
+            Object that represents newly created release.
+        """
+        raise NotImplementedError()
+
+    def save_archive(self, filename: str) -> None:
+        """
+        Save tarball of the release to requested `filename`.
+
+        Args:
+            filename: Path to the file to save archive to.
+        """
+        raise NotImplementedError()
+
+    def edit_release(self, name: str, message: str) -> None:
+        """
+        Edit name and message of a release.
+
+        Args:
+            name: Name of the release.
+            message: Description of the release.
+        """
+        raise NotImplementedError()
+
+
+class AuthMethod(str, Enum):
+    tokman = "tokman"
+    github_app = "github_app"
+    token = "token"
+
+
+class GitService(OgrAbstractClass):
+    """
+    Attributes:
+        instance_url (str): URL of the git forge instance.
+    """
+
+    instance_url: Optional[str] = None
+
+    def __init__(self, **_: Any) -> None:
+        pass
+
+    def __str__(self) -> str:
+        return f"GitService(instance_url={self.instance_url})"
+
+    def get_project(self, **kwargs: Any) -> "GitProject":
+        """
+        Get the requested project.
+
+        Args:
+            namespace (str): Namespace of the project.
+            user (str): Username of the project's owner.
+            repo (str): Repository name.
+
+        Returns:
+            Object that represents git project.
+        """
+        raise NotImplementedError
+
+    def get_project_from_url(self, url: str) -> "GitProject":
+        """
+        Args:
+            url: URL of the git repository.
+
+        Returns:
+            Object that represents project from the parsed URL.
+        """
+        repo_url = parse_git_repo(potential_url=url)
+        if not repo_url:
+            raise OgrException(f"Failed to find repository for url: {url}")
+        return self.get_project(repo=repo_url.repo, namespace=repo_url.namespace)
+
+    @_cached_property
+    def hostname(self) -> Optional[str]:
+        """Hostname of the service."""
+        raise NotImplementedError
+
+    @property
+    def user(self) -> "GitUser":
+        """User authenticated through the service."""
+        raise NotImplementedError
+
+    def change_token(self, new_token: str) -> None:
+        """
+        Change an API token. Only for the current instance and newly created projects.
+
+        Args:
+            new_token: New token to be set.
+        """
+        raise NotImplementedError
+
+    def set_auth_method(self, method: AuthMethod) -> None:
+        """
+        Override the default auth method.
+        Can be used when the service has more auth methods available.
+
+        Args:
+            method: the method identifier (a str name)
+        """
+        raise NotImplementedError()
+
+    def reset_auth_method(self) -> None:
+        """
+        Set the auth method to the default one.
+        """
+        raise NotImplementedError()
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "GitProject":
+        """
+        Create new project.
+
+        Args:
+            repo: Name of the newly created project.
+            namespace: Namespace of the newly created project.
+
+                Defaults to currently authenticated user.
+            description: Description of the newly created project.
+
+        Returns:
+            Object that represents newly created project.
+        """
+        raise NotImplementedError()
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list["GitProject"]:
+        """
+        List projects for given criteria.
+
+        Args:
+            namespace: Namespace to list projects from.
+            user: Login of the owner of the projects.
+            search_pattern: Regular expression that repository name should match.
+            language: Language to be present in the project, e.g. `"python"` or
+                `"html"`.
+        """
+        raise NotImplementedError
+
+
+class GitProject(OgrAbstractClass):
+    def __init__(self, repo: str, service: GitService, namespace: str) -> None:
+        """
+        Args:
+            repo: Name of the project.
+            service: GitService instance.
+            namespace: Namespace of the project.
+
+                - GitHub: username or org name.
+                - GitLab: username or org name.
+                - Pagure: namespace (e.g. `"rpms"`).
+
+                  In case of forks: `"fork/{username}/{namespace}"`.
+        """
+        self.service = service
+        self.repo = repo
+        self.namespace = namespace
+
+    def __str__(self) -> str:
+        return f"GitProject(namespace={self.namespace}, repo={self.repo}, service={self.service})"
+
+    @property
+    def description(self) -> str:
+        """
+        Returns:
+            Project description.
+        """
+        raise NotImplementedError()
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        """
+        Args:
+            new_description: description to set for project.
+        """
+        raise NotImplementedError()
+
+    def delete(self) -> None:
+        """Delete the project."""
+        raise NotImplementedError()
+
+    def exists(self) -> bool:
+        """
+        Check the existence of the repo.
+
+        Returns:
+            `True` if the project exists, `False` otherwise.
+        """
+        raise NotImplementedError()
+
+    def is_private(self) -> bool:
+        """
+        Is this repository private (accessible only by users with permissions).
+
+        Returns:
+            `True`, if the repository is private.
+        """
+        raise NotImplementedError()
+
+    def is_forked(self) -> bool:
+        """
+        Is this repository forked by the authenticated user?
+
+        Returns:
+            `True`, if the repository is fork.
+        """
+        raise NotImplementedError()
+
+    @property
+    def is_fork(self) -> bool:
+        """`True` if the project is a fork."""
+        raise NotImplementedError()
+
+    @property
+    def full_repo_name(self) -> str:
+        """Get repo name with namespace, e.g. `rpms/python-docker-py`."""
+        raise NotImplementedError()
+
+    @property
+    def parent(self) -> Optional["GitProject"]:
+        """Parent project if the project is a fork, otherwise `None`."""
+        raise NotImplementedError()
+
+    @property
+    def has_issues(self) -> bool:
+        """`True` if issues are enabled on the project."""
+        raise NotImplementedError()
+
+    def get_branches(self) -> list[str]:
+        """
+        Returns:
+            List with names of branches in the project.
+        """
+        raise NotImplementedError()
+
+    @property
+    def default_branch(self) -> str:
+        """Default branch (usually `main`, `master` or `trunk`)."""
+        raise NotImplementedError()
+
+    def get_description(self) -> str:
+        """
+        Returns:
+            Project description.
+        """
+        raise NotImplementedError()
+
+    def get_fork(self, create: bool = True) -> Optional["GitProject"]:
+        """
+        Provide GitProject instance of a fork of this project.
+
+        Args:
+            create: Create fork if it does not exist.
+
+        Returns:
+            `None` if the project is fork itself or there is no fork, otherwise
+            instance of a fork if is to be created or exists already.
+        """
+        raise NotImplementedError()
+
+    def get_owners(self) -> list[str]:
+        """
+        Returns:
+            List of usernames of project owners.
+        """
+        raise NotImplementedError()
+
+    def who_can_close_issue(self) -> set[str]:
+        """
+        Returns:
+            Names of all users who have permission to modify an issue.
+        """
+        raise NotImplementedError()
+
+    def who_can_merge_pr(self) -> set[str]:
+        """
+        Returns:
+            Names of all users who have permission to modify pull request.
+        """
+        raise NotImplementedError()
+
+    def which_groups_can_merge_pr(self) -> set[str]:
+        """
+        Returns:
+            Names of all groups that have permission to modify pull request.
+        """
+        raise NotImplementedError()
+
+    def can_merge_pr(self, username: str) -> bool:
+        """
+        Args:
+            username: Username.
+
+        Returns:
+            `True` if user merge pull request, `False` otherwise.
+        """
+        raise NotImplementedError()
+
+    def add_user(self, user: str, access_level: AccessLevel) -> None:
+        """
+        Add user to project.
+
+        Args:
+            user: Username of the user.
+            access_level: Permissions for the user.
+        """
+        raise NotImplementedError()
+
+    def remove_user(self, user: str) -> None:
+        """
+        Remove user from project.
+
+        Args:
+            user: Username of the user.
+        """
+        raise NotImplementedError()
+
+    def request_access(self) -> None:
+        """
+        Request an access to the project (cannot specify access level to be granted;
+        needs to be approved and specified by the user with maintainer/admin rights).
+        """
+        raise NotImplementedError()
+
+    def add_group(self, group: str, access_level: AccessLevel) -> None:
+        """
+        Add group to project.
+
+        Args:
+            group: Name of the group.
+            access_level: Permissions for the group.
+        """
+        raise NotImplementedError()
+
+    def remove_group(self, group: str) -> None:
+        """
+        Remove group from project.
+
+        Args:
+            group: Name of the group.
+        """
+        raise NotImplementedError()
+
+    def get_issue_list(
+        self,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        """
+        List of issues.
+
+        Args:
+            status: Status of the issues that are to be
+                included in the list.
+
+                Defaults to `IssueStatus.open`.
+            author: Username of the author of the issues.
+
+                Defaults to no filtering by author.
+            assignee: Username of the assignee on the issues.
+
+                Defaults to no filtering by assignees.
+            labels: Filter issues that have set specific labels.
+
+                Defaults to no filtering by labels.
+
+        Returns:
+            List of objects that represent requested issues.
+        """
+        raise NotImplementedError()
+
+    def get_issue(self, issue_id: int) -> "Issue":
+        """
+        Get issue.
+
+        Args:
+            issue_id: ID of the issue.
+
+        Returns:
+            Object that represents requested issue.
+        """
+        raise NotImplementedError()
+
+    def get_issue_info(self, issue_id: int) -> "Issue":
+        """
+        Get issue info.
+
+        Args:
+            issue_id: ID of the issue.
+
+        Returns:
+            Object that represents requested issue.
+        """
+        raise NotImplementedError()
+
+    def create_issue(
+        self,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> Issue:
+        """
+        Open new issue.
+
+        Args:
+            title: Title of the issue.
+            body: Description of the issue.
+            private: Is the new issue supposed to be confidential?
+
+                **Supported only by GitLab and Pagure.**
+
+                Defaults to unset.
+            labels: List of labels that are to be added to
+                the issue.
+
+                Defaults to no labels.
+            assignees: List of usernames of the assignees.
+
+                Defaults to no assignees.
+
+        Returns:
+            Object that represents newly created issue.
+
+        Raises:
+            IssueTrackerDisabled, if issue tracker is disabled.
+        """
+        raise NotImplementedError()
+
+    def get_pr_list(self, status: PRStatus = PRStatus.open) -> list["PullRequest"]:
+        """
+        List of pull requests.
+
+        Args:
+            status: Status of the pull requests that are to be included in the list.
+
+                Defaults to `PRStatus.open`.
+
+        Returns:
+            List of objects that represent pull requests with requested status.
+        """
+        raise NotImplementedError()
+
+    def get_pr(self, pr_id: int) -> "PullRequest":
+        """
+        Get pull request.
+
+        Args:
+            pr_id: ID of the pull request.
+
+        Returns:
+            Object that represents requested pull request.
+        """
+        raise NotImplementedError()
+
+    def get_pr_files_diff(
+        self,
+        pr_id: int,
+        retries: int = 0,
+        wait_seconds: int = 3,
+    ) -> dict:
+        """
+        Get files diff of a pull request.
+
+        Args:
+            pr_id: ID of the pull request.
+
+        Returns:
+            Dictionary representing files diff.
+        """
+        raise NotImplementedError()
+
+    def get_tags(self) -> list["GitTag"]:
+        """
+        Returns:
+            List of objects that represent tags.
+        """
+        raise NotImplementedError()
+
+    def get_sha_from_tag(self, tag_name: str) -> str:
+        """
+        Args:
+            tag_name: Name of the tag.
+
+        Returns:
+            Commit hash of the commit from the requested tag.
+        """
+        raise NotImplementedError()
+
+    def get_release(
+        self,
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> Release:
+        """
+        Get a single release.
+
+        Args:
+            identifier: Identifier of the release.
+
+                Defaults to `None`, which means not being used.
+            name: Name of the release.
+
+                Defaults to `None`, which means not being used.
+            tag_name: Tag that the release is tied to.
+
+                Defaults to `None`, which means not being used.
+
+        Returns:
+            Object that represents release that satisfies requested condition.
+        """
+        raise NotImplementedError()
+
+    def get_latest_release(self) -> Optional[Release]:
+        """
+        Returns:
+            Object that represents the latest release.
+        """
+        raise NotImplementedError()
+
+    def get_releases(self) -> list[Release]:
+        """
+        Returns:
+            List of the objects that represent releases.
+        """
+        raise NotImplementedError()
+
+    def create_release(
+        self,
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> Release:
+        """
+        Create new release.
+
+        Args:
+            tag: Tag which is the release based off.
+            name: Name of the release.
+            message: Message or description of the release.
+            ref: Git reference, mainly commit hash for the release. If provided
+                git tag is created prior to creating a release.
+
+                Defaults to `None`.
+
+        Returns:
+            Object that represents newly created release.
+        """
+        raise NotImplementedError()
+
+    def create_pr(
+        self,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        Create new pull request.
+
+        Args:
+            title: Title of the pull request.
+            body: Description of the pull request.
+            target_branch: Name of the branch where the changes are merged.
+            source_branch: Name of the branch from which the changes are pulled.
+            fork_username: The username of forked repository.
+
+                Defaults to `None`.
+
+        Returns:
+            Object that represents newly created pull request.
+        """
+        raise NotImplementedError()
+
+    def commit_comment(
+        self,
+        commit: str,
+        body: str,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "CommitComment":
+        """
+        Add new comment to a commit.
+
+        Args:
+            commit: Hash of the commit.
+            body: Body of the comment.
+            filename: Name of the file that is related to the comment.
+
+                Defaults to `None`, which means no relation to file.
+            row: Number of the row that the comment is related to.
+
+                Defaults to `None`, which means no relation to the row.
+
+        Returns:
+            Object that represents newly created commit comment.
+        """
+        raise NotImplementedError()
+
+    def get_commit_comments(self, commit: str) -> list[CommitComment]:
+        """
+        Get comments for a commit.
+
+        Args:
+            commit: The hash of the commit.
+
+        Returns:
+            List of all comments for the commit.
+        """
+        raise NotImplementedError()
+
+    def set_commit_status(
+        self,
+        commit: str,
+        state: Union[CommitStatus, str],
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        """
+        Create a status on a commit.
+
+        Args:
+            commit: The hash of the commit.
+            state: The state of the status.
+            target_url: The target URL to associate with this status.
+            description: A short description of the status.
+            context: A label to differentiate this status from the status of other systems.
+            trim: Whether to trim the description to 140 characters.
+
+                Defaults to `False`.
+
+        Returns:
+            Object that represents created commit status.
+        """
+        raise NotImplementedError()
+
+    def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+        """
+        Get statuses of the commit.
+
+        Args:
+            commit: Hash of the commit.
+
+        Returns:
+            List of all commit statuses on the commit.
+        """
+        raise NotImplementedError()
+
+    def get_git_urls(self) -> dict[str, str]:
+        """
+        Get git URLs for the project.
+
+        Returns:
+            Dictionary with at least SSH and HTTP URLs for the current project.
+        """
+        raise NotImplementedError()
+
+    def fork_create(self, namespace: Optional[str] = None) -> "GitProject":
+        """
+        Fork this project using the authenticated user.
+
+        Args:
+            namespace: Namespace where the project should be forked.
+
+                Defaults to `None`, which means forking to the namespace of
+                currently authenticated user.
+
+        Returns:
+            Fork of the current project.
+
+        Raises:
+            In case the fork already exists.
+        """
+        raise NotImplementedError()
+
+    def change_token(self, new_token: str) -> None:
+        """
+        Change an API token. Only for the current instance.
+
+        Args:
+            new_token: New token to be set.
+        """
+        raise NotImplementedError
+
+    def get_file_content(self, path: str, ref: Optional[str] = None) -> str:
+        """
+        Get a content of the file in the repo.
+
+        Args:
+            path: Path to the file.
+            ref: Branch or commit.
+
+                Defaults to repo's default branch.
+
+        Returns:
+            Contents of the file as string.
+
+        Raises:
+            FileNotFoundError: if there is no such file.
+        """
+        raise NotImplementedError
+
+    def get_files(
+        self,
+        ref: Optional[str] = None,
+        filter_regex: Optional[str] = None,
+        recursive: bool = False,
+    ) -> list[str]:
+        """
+        Get a list of file paths of the repo.
+
+        Args:
+            ref: Branch or commit.
+
+                Defaults to repo's default branch.
+            filter_regex: Filter the paths with `re.search`.
+
+                Defaults to `None`, which means no filtering.
+            recursive: Whether to return only top directory files
+                or all files recursively.
+
+                Defaults to `False`, which means only top-level directory.
+
+        Returns:
+            List of paths of the files in the repo.
+        """
+        raise NotImplementedError
+
+    def get_forks(self) -> Sequence["GitProject"]:
+        """
+        Returns:
+            All forks of the project.
+        """
+        raise NotImplementedError()
+
+    def get_web_url(self) -> str:
+        """
+        Returns:
+            Web URL of the project.
+        """
+        raise NotImplementedError()
+
+    def get_sha_from_branch(self, branch: str) -> Optional[str]:
+        """
+        Returns:
+            Commit SHA of head of the branch. `None` if no branch was found.
+        """
+        raise NotImplementedError()
+
+    def get_contributors(self) -> set[str]:
+        """
+        Returns:
+            Set of all contributors to the given project.
+        """
+        raise NotImplementedError()
+
+    def users_with_write_access(self) -> set[str]:
+        """
+        Returns:
+            List of users who have write access to the project
+        """
+        raise NotImplementedError("Use subclass instead.")
+
+    def has_write_access(self, user: str) -> bool:
+        """
+        Decides whether a given user has write access to the project.
+
+        Args:
+            user: The user we are going to check to see if he/she has access
+        """
+        return user in self.users_with_write_access()
+
+
+class GitUser(OgrAbstractClass):
+    """
+    Represents currently authenticated user through service.
+    """
+
+    def __init__(self, service: GitService) -> None:
+        self.service = service
+
+    def get_username(self) -> str:
+        """
+        Returns:
+            Login of the user.
+        """
+        raise NotImplementedError()
+
+    def get_email(self) -> str:
+        """
+        Returns:
+            Email of the user.
+        """
+        raise NotImplementedError()
+
+    def get_projects(self) -> Sequence["GitProject"]:
+        """
+        Returns:
+            Sequence of projects in user's namespace.
+        """
+        raise NotImplementedError()
+
+    def get_forks(self) -> Sequence["GitProject"]:
+        """
+        Returns:
+            Sequence of forks in user's namespace.
+        """
+        raise NotImplementedError()
+
+
+
+
+
+
+
+

Functions

+
+
+def catch_common_exceptions(function: Callable) ‑> Any +
+
+

Decorator catching common exceptions.

+

Args

+
+
function : Callable
+
Function or method to decorate.
+
+

Raises

+

GithubAPIException, if authentication to Github failed. +GitlabAPIException, if authentication to Gitlab failed. +OgrNetworkError, if network problems occurred while performing a request.

+
+ +Expand source code + +
def catch_common_exceptions(function: Callable) -> Any:
+    """
+    Decorator catching common exceptions.
+
+    Args:
+        function (Callable): Function or method to decorate.
+
+    Raises:
+        GithubAPIException, if authentication to Github failed.
+        GitlabAPIException, if authentication to Gitlab failed.
+        OgrNetworkError, if network problems occurred while performing a request.
+    """
+
+    @functools.wraps(function)
+    def wrapper(*args, **kwargs):
+        try:
+            return function(*args, **kwargs)
+        except github.BadCredentialsException as ex:
+            raise GithubAPIException("Invalid Github credentials") from ex
+        except gitlab.GitlabAuthenticationError as ex:
+            raise GitlabAPIException("Invalid Gitlab credentials") from ex
+        except requests.exceptions.ConnectionError as ex:
+            raise OgrNetworkError(
+                "Could not perform the request due to a network error",
+            ) from ex
+        except APIException as ex:
+            __check_for_internal_failure(ex)
+        except (github.GithubException, gitlab.GitlabError) as ex:
+            __check_for_internal_failure(__wrap_exception(ex))
+
+    return wrapper
+
+
+
+
+
+

Classes

+
+
+class AccessLevel +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Enumeration representing an access level to the repository.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Value from enumerationGitHubGitLabPagure
AccessLevel.pullpullguestticket
AccessLevel.triagetriagereporterticket
AccessLevel.pushpushdevelopercommit
AccessLevel.adminadminmaintainercommit
AccessLevel.maintainmaintainowner (only for groups)admin
+
+ +Expand source code + +
class AccessLevel(IntEnum):
+    """
+    Enumeration representing an access level to the repository.
+
+    | Value from enumeration | GitHub   | GitLab                  | Pagure |
+    | ---------------------- | -------- | ----------------------- | ------ |
+    | `AccessLevel.pull`     | pull     | guest                   | ticket |
+    | `AccessLevel.triage`   | triage   | reporter                | ticket |
+    | `AccessLevel.push`     | push     | developer               | commit |
+    | `AccessLevel.admin`    | admin    | maintainer              | commit |
+    | `AccessLevel.maintain` | maintain | owner (only for groups) | admin  |
+    """
+
+    pull = 1
+    triage = 2
+    push = 3
+    admin = 4
+    maintain = 5
+
+

Ancestors

+
    +
  • enum.IntEnum
  • +
  • builtins.int
  • +
  • enum.Enum
  • +
+

Class variables

+
+
var admin
+
+
+
+
var maintain
+
+
+
+
var pull
+
+
+
+
var push
+
+
+
+
var triage
+
+
+
+
+
+
+class AuthMethod +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

An enumeration.

+
+ +Expand source code + +
class AuthMethod(str, Enum):
+    tokman = "tokman"
+    github_app = "github_app"
+    token = "token"
+
+

Ancestors

+
    +
  • builtins.str
  • +
  • enum.Enum
  • +
+

Class variables

+
+
var github_app
+
+
+
+
var token
+
+
+
+
var tokman
+
+
+
+
+
+
+class CatchCommonErrors +(*args, **kwargs) +
+
+

A metaclass wrapping methods with a common exception handler.

+

This handler catches exceptions which can occur almost anywhere +and catching them manually would be tedious and converts them +to an appropriate ogr exception for the user. This includes +exceptions such as: +- authentication (from Github/Gitlab) +- network errors

+
+ +Expand source code + +
class CatchCommonErrors(type):
+    """
+    A metaclass wrapping methods with a common exception handler.
+
+    This handler catches exceptions which can occur almost anywhere
+    and catching them manually would be tedious and converts them
+    to an appropriate ogr exception for the user. This includes
+    exceptions such as:
+        - authentication (from Github/Gitlab)
+        - network errors
+    """
+
+    def __new__(cls, name, bases, namespace):
+        for key, value in namespace.items():
+            # There is an anticipated change in behaviour in Python 3.10
+            # for static/class methods. From Python 3.10 they will be callable.
+            # We need to achieve consistent behaviour with older versions,
+            # hence the explicit handling is needed here (isinstance checking
+            # works the same). Moreover, static/class method decorator must
+            # be used last, especially prior to Python 3.10 since they return
+            # descriptor objects and not functions.
+            # See: https://bugs.python.org/issue43682
+            if isinstance(value, staticmethod):
+                namespace[key] = staticmethod(catch_common_exceptions(value.__func__))
+            elif isinstance(value, classmethod):
+                namespace[key] = classmethod(catch_common_exceptions(value.__func__))
+            elif callable(namespace[key]):
+                namespace[key] = catch_common_exceptions(namespace[key])
+        return super().__new__(cls, name, bases, namespace)
+
+

Ancestors

+
    +
  • builtins.type
  • +
+
+
+class Comment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class Comment(OgrAbstractClass):
+    def __init__(
+        self,
+        raw_comment: Optional[Any] = None,
+        parent: Optional[Any] = None,
+        body: Optional[str] = None,
+        id_: Optional[int] = None,
+        author: Optional[str] = None,
+        created: Optional[datetime.datetime] = None,
+        edited: Optional[datetime.datetime] = None,
+    ) -> None:
+        if raw_comment:
+            self._from_raw_comment(raw_comment)
+        elif body and author:
+            self._body = body
+            self._id = id_
+            self._author = author
+            self._created = created
+            self._edited = edited
+        else:
+            raise ValueError("cannot construct comment without body and author")
+
+        self._parent = parent
+
+    def __str__(self) -> str:
+        body = f"{self.body[:10]}..." if self.body is not None else "None"
+        return (
+            f"Comment("
+            f"comment='{body}', "
+            f"author='{self.author}', "
+            f"created='{self.created}', "
+            f"edited='{self.edited}')"
+        )
+
+    def _from_raw_comment(self, raw_comment: Any) -> None:
+        """Constructs Comment object from raw_comment given from API."""
+        raise NotImplementedError()
+
+    @property
+    def body(self) -> str:
+        """Body of the comment."""
+        return self._body
+
+    @body.setter
+    def body(self, new_body: str) -> None:
+        self._body = new_body
+
+    @property
+    def id(self) -> int:
+        return self._id
+
+    @property
+    def author(self) -> str:
+        """Login of the author of the comment."""
+        return self._author
+
+    @property
+    def created(self) -> datetime.datetime:
+        """Datetime of creation of the comment."""
+        return self._created
+
+    @property
+    def edited(self) -> datetime.datetime:
+        """Datetime of last edit of the comment."""
+        return self._edited
+
+    def get_reactions(self) -> list[Reaction]:
+        """Returns list of reactions."""
+        raise NotImplementedError()
+
+    def add_reaction(self, reaction: str) -> Reaction:
+        """
+        Reacts to a comment.
+
+        Colons in between reaction are not needed, e.g. `comment.add_reaction("+1")`.
+
+        Args:
+            reaction: String representing specific reaction to be added.
+
+        Returns:
+            Object representing newly added reaction.
+        """
+        raise NotImplementedError()
+
+

Ancestors

+ +

Subclasses

+ +

Instance variables

+
+
var author : str
+
+

Login of the author of the comment.

+
+ +Expand source code + +
@property
+def author(self) -> str:
+    """Login of the author of the comment."""
+    return self._author
+
+
+
var body : str
+
+

Body of the comment.

+
+ +Expand source code + +
@property
+def body(self) -> str:
+    """Body of the comment."""
+    return self._body
+
+
+
var created : datetime.datetime
+
+

Datetime of creation of the comment.

+
+ +Expand source code + +
@property
+def created(self) -> datetime.datetime:
+    """Datetime of creation of the comment."""
+    return self._created
+
+
+
var edited : datetime.datetime
+
+

Datetime of last edit of the comment.

+
+ +Expand source code + +
@property
+def edited(self) -> datetime.datetime:
+    """Datetime of last edit of the comment."""
+    return self._edited
+
+
+
var id : int
+
+
+
+ +Expand source code + +
@property
+def id(self) -> int:
+    return self._id
+
+
+
+

Methods

+
+
+def add_reaction(self, reaction: str) ‑> Reaction +
+
+

Reacts to a comment.

+

Colons in between reaction are not needed, e.g. comment.add_reaction("+1").

+

Args

+
+
reaction
+
String representing specific reaction to be added.
+
+

Returns

+

Object representing newly added reaction.

+
+ +Expand source code + +
def add_reaction(self, reaction: str) -> Reaction:
+    """
+    Reacts to a comment.
+
+    Colons in between reaction are not needed, e.g. `comment.add_reaction("+1")`.
+
+    Args:
+        reaction: String representing specific reaction to be added.
+
+    Returns:
+        Object representing newly added reaction.
+    """
+    raise NotImplementedError()
+
+
+
+def get_reactions(self) ‑> list[Reaction] +
+
+

Returns list of reactions.

+
+ +Expand source code + +
def get_reactions(self) -> list[Reaction]:
+    """Returns list of reactions."""
+    raise NotImplementedError()
+
+
+
+
+
+class CommitComment +(sha: str, body: str, author: str) +
+
+

Attributes

+
+
sha : str
+
Hash of the related commit.
+
body : str
+
Body of the comment.
+
author : str
+
Login of the author.
+
+
+ +Expand source code + +
class CommitComment(OgrAbstractClass):
+    """
+    Attributes:
+        sha (str): Hash of the related commit.
+        body (str): Body of the comment.
+        author (str): Login of the author.
+    """
+
+    def __init__(self, sha: str, body: str, author: str) -> None:
+        self.sha = sha
+        self.body = body
+        self.author = author
+
+    @property  # type: ignore
+    @deprecate_and_set_removal(
+        since="0.41.0",
+        remove_in="0.46.0 (or 1.0.0 if it comes sooner)",
+        message="Use body",
+    )
+    def comment(self) -> str:
+        return self.body
+
+    def __str__(self) -> str:
+        return (
+            f"CommitComment(commit={self.sha}, author={self.author}, body={self.body})"
+        )
+
+

Ancestors

+ +

Instance variables

+
+
var comment : str
+
+
+
+ +Expand source code + +
@property  # type: ignore
+@deprecate_and_set_removal(
+    since="0.41.0",
+    remove_in="0.46.0 (or 1.0.0 if it comes sooner)",
+    message="Use body",
+)
+def comment(self) -> str:
+    return self.body
+
+
+
+
+
+class CommitFlag +(raw_commit_flag: Optional[Any] = None, project: Optional[ForwardRef('GitProject')] = None, commit: Optional[str] = None, state: Optional[CommitStatus] = None, context: Optional[str] = None, comment: Optional[str] = None, uid: Optional[str] = None, url: Optional[str] = None) +
+
+
+
+ +Expand source code + +
class CommitFlag(OgrAbstractClass):
+    _states: ClassVar[dict[str, CommitStatus]] = {}
+
+    def __init__(
+        self,
+        raw_commit_flag: Optional[Any] = None,
+        project: Optional["GitProject"] = None,
+        commit: Optional[str] = None,
+        state: Optional[CommitStatus] = None,
+        context: Optional[str] = None,
+        comment: Optional[str] = None,
+        uid: Optional[str] = None,
+        url: Optional[str] = None,
+    ) -> None:
+        self.uid = uid
+        self.project = project
+        self.commit = commit
+
+        if commit and state and context:
+            self.state = state
+            self.context = context
+            self.comment = comment
+            self.url = url
+        else:
+            self._raw_commit_flag = raw_commit_flag
+            self._from_raw_commit_flag()
+
+    def __str__(self) -> str:
+        return (
+            f"CommitFlag("
+            f"commit='{self.commit}', "
+            f"state='{self.state.name}', "
+            f"context='{self.context}', "
+            f"uid='{self.uid}', "
+            f"comment='{self.comment}', "
+            f"url='{self.url}', "
+            f"created='{self.created}', "
+            f"edited='{self.edited}')"
+        )
+
+    @classmethod
+    def _state_from_str(cls, state: str) -> CommitStatus:
+        """
+        Transforms state from string to enumeration.
+
+        Args:
+            state: String representation of a state.
+
+        Returns:
+            Commit status.
+        """
+        raise NotImplementedError()
+
+    @classmethod
+    def _validate_state(cls, state: CommitStatus) -> CommitStatus:
+        """
+        Validates state of the commit status (if it can be used with forge).
+        """
+        raise NotImplementedError()
+
+    def _from_raw_commit_flag(self) -> None:
+        """
+        Sets attributes based on the raw flag that has been given through constructor.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get(project: Any, commit: str) -> list["CommitFlag"]:
+        """
+        Acquire commit statuses for given commit in the project.
+
+        Args:
+            project (GitProject): Project where the commit is located.
+            commit: Commit hash for which we request statuses.
+
+        Returns:
+            List of commit statuses for the commit.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def set(
+        project: Any,
+        commit: str,
+        state: CommitStatus,
+        target_url: str,
+        description: str,
+        context: str,
+    ) -> "CommitFlag":
+        """
+        Set a new commit status.
+
+        Args:
+            project (GitProject): Project where the commit is located.
+            commit: Commit hash for which we set status.
+            state: State for the commit status.
+            target_url: URL for the commit status.
+            description: Description of the commit status.
+            context: Identifier to group related commit statuses.
+        """
+        raise NotImplementedError()
+
+    @property
+    def created(self) -> datetime.datetime:
+        """Datetime of creating the commit status."""
+        raise NotImplementedError()
+
+    @property
+    def edited(self) -> datetime.datetime:
+        """Datetime of editing the commit status."""
+        raise NotImplementedError()
+
+

Ancestors

+ +

Subclasses

+ +

Static methods

+
+
+def get(project: Any, commit: str) ‑> list['CommitFlag'] +
+
+

Acquire commit statuses for given commit in the project.

+

Args

+
+
project : GitProject
+
Project where the commit is located.
+
commit
+
Commit hash for which we request statuses.
+
+

Returns

+

List of commit statuses for the commit.

+
+ +Expand source code + +
@staticmethod
+def get(project: Any, commit: str) -> list["CommitFlag"]:
+    """
+    Acquire commit statuses for given commit in the project.
+
+    Args:
+        project (GitProject): Project where the commit is located.
+        commit: Commit hash for which we request statuses.
+
+    Returns:
+        List of commit statuses for the commit.
+    """
+    raise NotImplementedError()
+
+
+
+def set(project: Any, commit: str, state: CommitStatus, target_url: str, description: str, context: str) ‑> CommitFlag +
+
+

Set a new commit status.

+

Args

+
+
project : GitProject
+
Project where the commit is located.
+
commit
+
Commit hash for which we set status.
+
state
+
State for the commit status.
+
target_url
+
URL for the commit status.
+
description
+
Description of the commit status.
+
context
+
Identifier to group related commit statuses.
+
+
+ +Expand source code + +
@staticmethod
+def set(
+    project: Any,
+    commit: str,
+    state: CommitStatus,
+    target_url: str,
+    description: str,
+    context: str,
+) -> "CommitFlag":
+    """
+    Set a new commit status.
+
+    Args:
+        project (GitProject): Project where the commit is located.
+        commit: Commit hash for which we set status.
+        state: State for the commit status.
+        target_url: URL for the commit status.
+        description: Description of the commit status.
+        context: Identifier to group related commit statuses.
+    """
+    raise NotImplementedError()
+
+
+
+

Instance variables

+
+
var created : datetime.datetime
+
+

Datetime of creating the commit status.

+
+ +Expand source code + +
@property
+def created(self) -> datetime.datetime:
+    """Datetime of creating the commit status."""
+    raise NotImplementedError()
+
+
+
var edited : datetime.datetime
+
+

Datetime of editing the commit status.

+
+ +Expand source code + +
@property
+def edited(self) -> datetime.datetime:
+    """Datetime of editing the commit status."""
+    raise NotImplementedError()
+
+
+
+
+
+class CommitStatus +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Enumeration that represents possible state of commit statuses.

+
+ +Expand source code + +
class CommitStatus(Enum):
+    """Enumeration that represents possible state of commit statuses."""
+
+    pending = 1
+    success = 2
+    failure = 3
+    error = 4
+    canceled = 5
+    running = 6
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var canceled
+
+
+
+
var error
+
+
+
+
var failure
+
+
+
+
var pending
+
+
+
+
var running
+
+
+
+
var success
+
+
+
+
+
+
+class GitProject +(repo: str, service: GitService, namespace: str) +
+
+

Args

+
+
repo
+
Name of the project.
+
service
+
GitService instance.
+
namespace
+
+

Namespace of the project.

+
    +
  • GitHub: username or org name.
  • +
  • GitLab: username or org name.
  • +
  • Pagure: namespace (e.g. "rpms").
  • +
+

In case of forks: "fork/{username}/{namespace}".

+
+
+
+ +Expand source code + +
class GitProject(OgrAbstractClass):
+    def __init__(self, repo: str, service: GitService, namespace: str) -> None:
+        """
+        Args:
+            repo: Name of the project.
+            service: GitService instance.
+            namespace: Namespace of the project.
+
+                - GitHub: username or org name.
+                - GitLab: username or org name.
+                - Pagure: namespace (e.g. `"rpms"`).
+
+                  In case of forks: `"fork/{username}/{namespace}"`.
+        """
+        self.service = service
+        self.repo = repo
+        self.namespace = namespace
+
+    def __str__(self) -> str:
+        return f"GitProject(namespace={self.namespace}, repo={self.repo}, service={self.service})"
+
+    @property
+    def description(self) -> str:
+        """
+        Returns:
+            Project description.
+        """
+        raise NotImplementedError()
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        """
+        Args:
+            new_description: description to set for project.
+        """
+        raise NotImplementedError()
+
+    def delete(self) -> None:
+        """Delete the project."""
+        raise NotImplementedError()
+
+    def exists(self) -> bool:
+        """
+        Check the existence of the repo.
+
+        Returns:
+            `True` if the project exists, `False` otherwise.
+        """
+        raise NotImplementedError()
+
+    def is_private(self) -> bool:
+        """
+        Is this repository private (accessible only by users with permissions).
+
+        Returns:
+            `True`, if the repository is private.
+        """
+        raise NotImplementedError()
+
+    def is_forked(self) -> bool:
+        """
+        Is this repository forked by the authenticated user?
+
+        Returns:
+            `True`, if the repository is fork.
+        """
+        raise NotImplementedError()
+
+    @property
+    def is_fork(self) -> bool:
+        """`True` if the project is a fork."""
+        raise NotImplementedError()
+
+    @property
+    def full_repo_name(self) -> str:
+        """Get repo name with namespace, e.g. `rpms/python-docker-py`."""
+        raise NotImplementedError()
+
+    @property
+    def parent(self) -> Optional["GitProject"]:
+        """Parent project if the project is a fork, otherwise `None`."""
+        raise NotImplementedError()
+
+    @property
+    def has_issues(self) -> bool:
+        """`True` if issues are enabled on the project."""
+        raise NotImplementedError()
+
+    def get_branches(self) -> list[str]:
+        """
+        Returns:
+            List with names of branches in the project.
+        """
+        raise NotImplementedError()
+
+    @property
+    def default_branch(self) -> str:
+        """Default branch (usually `main`, `master` or `trunk`)."""
+        raise NotImplementedError()
+
+    def get_description(self) -> str:
+        """
+        Returns:
+            Project description.
+        """
+        raise NotImplementedError()
+
+    def get_fork(self, create: bool = True) -> Optional["GitProject"]:
+        """
+        Provide GitProject instance of a fork of this project.
+
+        Args:
+            create: Create fork if it does not exist.
+
+        Returns:
+            `None` if the project is fork itself or there is no fork, otherwise
+            instance of a fork if is to be created or exists already.
+        """
+        raise NotImplementedError()
+
+    def get_owners(self) -> list[str]:
+        """
+        Returns:
+            List of usernames of project owners.
+        """
+        raise NotImplementedError()
+
+    def who_can_close_issue(self) -> set[str]:
+        """
+        Returns:
+            Names of all users who have permission to modify an issue.
+        """
+        raise NotImplementedError()
+
+    def who_can_merge_pr(self) -> set[str]:
+        """
+        Returns:
+            Names of all users who have permission to modify pull request.
+        """
+        raise NotImplementedError()
+
+    def which_groups_can_merge_pr(self) -> set[str]:
+        """
+        Returns:
+            Names of all groups that have permission to modify pull request.
+        """
+        raise NotImplementedError()
+
+    def can_merge_pr(self, username: str) -> bool:
+        """
+        Args:
+            username: Username.
+
+        Returns:
+            `True` if user merge pull request, `False` otherwise.
+        """
+        raise NotImplementedError()
+
+    def add_user(self, user: str, access_level: AccessLevel) -> None:
+        """
+        Add user to project.
+
+        Args:
+            user: Username of the user.
+            access_level: Permissions for the user.
+        """
+        raise NotImplementedError()
+
+    def remove_user(self, user: str) -> None:
+        """
+        Remove user from project.
+
+        Args:
+            user: Username of the user.
+        """
+        raise NotImplementedError()
+
+    def request_access(self) -> None:
+        """
+        Request an access to the project (cannot specify access level to be granted;
+        needs to be approved and specified by the user with maintainer/admin rights).
+        """
+        raise NotImplementedError()
+
+    def add_group(self, group: str, access_level: AccessLevel) -> None:
+        """
+        Add group to project.
+
+        Args:
+            group: Name of the group.
+            access_level: Permissions for the group.
+        """
+        raise NotImplementedError()
+
+    def remove_group(self, group: str) -> None:
+        """
+        Remove group from project.
+
+        Args:
+            group: Name of the group.
+        """
+        raise NotImplementedError()
+
+    def get_issue_list(
+        self,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        """
+        List of issues.
+
+        Args:
+            status: Status of the issues that are to be
+                included in the list.
+
+                Defaults to `IssueStatus.open`.
+            author: Username of the author of the issues.
+
+                Defaults to no filtering by author.
+            assignee: Username of the assignee on the issues.
+
+                Defaults to no filtering by assignees.
+            labels: Filter issues that have set specific labels.
+
+                Defaults to no filtering by labels.
+
+        Returns:
+            List of objects that represent requested issues.
+        """
+        raise NotImplementedError()
+
+    def get_issue(self, issue_id: int) -> "Issue":
+        """
+        Get issue.
+
+        Args:
+            issue_id: ID of the issue.
+
+        Returns:
+            Object that represents requested issue.
+        """
+        raise NotImplementedError()
+
+    def get_issue_info(self, issue_id: int) -> "Issue":
+        """
+        Get issue info.
+
+        Args:
+            issue_id: ID of the issue.
+
+        Returns:
+            Object that represents requested issue.
+        """
+        raise NotImplementedError()
+
+    def create_issue(
+        self,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> Issue:
+        """
+        Open new issue.
+
+        Args:
+            title: Title of the issue.
+            body: Description of the issue.
+            private: Is the new issue supposed to be confidential?
+
+                **Supported only by GitLab and Pagure.**
+
+                Defaults to unset.
+            labels: List of labels that are to be added to
+                the issue.
+
+                Defaults to no labels.
+            assignees: List of usernames of the assignees.
+
+                Defaults to no assignees.
+
+        Returns:
+            Object that represents newly created issue.
+
+        Raises:
+            IssueTrackerDisabled, if issue tracker is disabled.
+        """
+        raise NotImplementedError()
+
+    def get_pr_list(self, status: PRStatus = PRStatus.open) -> list["PullRequest"]:
+        """
+        List of pull requests.
+
+        Args:
+            status: Status of the pull requests that are to be included in the list.
+
+                Defaults to `PRStatus.open`.
+
+        Returns:
+            List of objects that represent pull requests with requested status.
+        """
+        raise NotImplementedError()
+
+    def get_pr(self, pr_id: int) -> "PullRequest":
+        """
+        Get pull request.
+
+        Args:
+            pr_id: ID of the pull request.
+
+        Returns:
+            Object that represents requested pull request.
+        """
+        raise NotImplementedError()
+
+    def get_pr_files_diff(
+        self,
+        pr_id: int,
+        retries: int = 0,
+        wait_seconds: int = 3,
+    ) -> dict:
+        """
+        Get files diff of a pull request.
+
+        Args:
+            pr_id: ID of the pull request.
+
+        Returns:
+            Dictionary representing files diff.
+        """
+        raise NotImplementedError()
+
+    def get_tags(self) -> list["GitTag"]:
+        """
+        Returns:
+            List of objects that represent tags.
+        """
+        raise NotImplementedError()
+
+    def get_sha_from_tag(self, tag_name: str) -> str:
+        """
+        Args:
+            tag_name: Name of the tag.
+
+        Returns:
+            Commit hash of the commit from the requested tag.
+        """
+        raise NotImplementedError()
+
+    def get_release(
+        self,
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> Release:
+        """
+        Get a single release.
+
+        Args:
+            identifier: Identifier of the release.
+
+                Defaults to `None`, which means not being used.
+            name: Name of the release.
+
+                Defaults to `None`, which means not being used.
+            tag_name: Tag that the release is tied to.
+
+                Defaults to `None`, which means not being used.
+
+        Returns:
+            Object that represents release that satisfies requested condition.
+        """
+        raise NotImplementedError()
+
+    def get_latest_release(self) -> Optional[Release]:
+        """
+        Returns:
+            Object that represents the latest release.
+        """
+        raise NotImplementedError()
+
+    def get_releases(self) -> list[Release]:
+        """
+        Returns:
+            List of the objects that represent releases.
+        """
+        raise NotImplementedError()
+
+    def create_release(
+        self,
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> Release:
+        """
+        Create new release.
+
+        Args:
+            tag: Tag which is the release based off.
+            name: Name of the release.
+            message: Message or description of the release.
+            ref: Git reference, mainly commit hash for the release. If provided
+                git tag is created prior to creating a release.
+
+                Defaults to `None`.
+
+        Returns:
+            Object that represents newly created release.
+        """
+        raise NotImplementedError()
+
+    def create_pr(
+        self,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        Create new pull request.
+
+        Args:
+            title: Title of the pull request.
+            body: Description of the pull request.
+            target_branch: Name of the branch where the changes are merged.
+            source_branch: Name of the branch from which the changes are pulled.
+            fork_username: The username of forked repository.
+
+                Defaults to `None`.
+
+        Returns:
+            Object that represents newly created pull request.
+        """
+        raise NotImplementedError()
+
+    def commit_comment(
+        self,
+        commit: str,
+        body: str,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "CommitComment":
+        """
+        Add new comment to a commit.
+
+        Args:
+            commit: Hash of the commit.
+            body: Body of the comment.
+            filename: Name of the file that is related to the comment.
+
+                Defaults to `None`, which means no relation to file.
+            row: Number of the row that the comment is related to.
+
+                Defaults to `None`, which means no relation to the row.
+
+        Returns:
+            Object that represents newly created commit comment.
+        """
+        raise NotImplementedError()
+
+    def get_commit_comments(self, commit: str) -> list[CommitComment]:
+        """
+        Get comments for a commit.
+
+        Args:
+            commit: The hash of the commit.
+
+        Returns:
+            List of all comments for the commit.
+        """
+        raise NotImplementedError()
+
+    def set_commit_status(
+        self,
+        commit: str,
+        state: Union[CommitStatus, str],
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        """
+        Create a status on a commit.
+
+        Args:
+            commit: The hash of the commit.
+            state: The state of the status.
+            target_url: The target URL to associate with this status.
+            description: A short description of the status.
+            context: A label to differentiate this status from the status of other systems.
+            trim: Whether to trim the description to 140 characters.
+
+                Defaults to `False`.
+
+        Returns:
+            Object that represents created commit status.
+        """
+        raise NotImplementedError()
+
+    def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+        """
+        Get statuses of the commit.
+
+        Args:
+            commit: Hash of the commit.
+
+        Returns:
+            List of all commit statuses on the commit.
+        """
+        raise NotImplementedError()
+
+    def get_git_urls(self) -> dict[str, str]:
+        """
+        Get git URLs for the project.
+
+        Returns:
+            Dictionary with at least SSH and HTTP URLs for the current project.
+        """
+        raise NotImplementedError()
+
+    def fork_create(self, namespace: Optional[str] = None) -> "GitProject":
+        """
+        Fork this project using the authenticated user.
+
+        Args:
+            namespace: Namespace where the project should be forked.
+
+                Defaults to `None`, which means forking to the namespace of
+                currently authenticated user.
+
+        Returns:
+            Fork of the current project.
+
+        Raises:
+            In case the fork already exists.
+        """
+        raise NotImplementedError()
+
+    def change_token(self, new_token: str) -> None:
+        """
+        Change an API token. Only for the current instance.
+
+        Args:
+            new_token: New token to be set.
+        """
+        raise NotImplementedError
+
+    def get_file_content(self, path: str, ref: Optional[str] = None) -> str:
+        """
+        Get a content of the file in the repo.
+
+        Args:
+            path: Path to the file.
+            ref: Branch or commit.
+
+                Defaults to repo's default branch.
+
+        Returns:
+            Contents of the file as string.
+
+        Raises:
+            FileNotFoundError: if there is no such file.
+        """
+        raise NotImplementedError
+
+    def get_files(
+        self,
+        ref: Optional[str] = None,
+        filter_regex: Optional[str] = None,
+        recursive: bool = False,
+    ) -> list[str]:
+        """
+        Get a list of file paths of the repo.
+
+        Args:
+            ref: Branch or commit.
+
+                Defaults to repo's default branch.
+            filter_regex: Filter the paths with `re.search`.
+
+                Defaults to `None`, which means no filtering.
+            recursive: Whether to return only top directory files
+                or all files recursively.
+
+                Defaults to `False`, which means only top-level directory.
+
+        Returns:
+            List of paths of the files in the repo.
+        """
+        raise NotImplementedError
+
+    def get_forks(self) -> Sequence["GitProject"]:
+        """
+        Returns:
+            All forks of the project.
+        """
+        raise NotImplementedError()
+
+    def get_web_url(self) -> str:
+        """
+        Returns:
+            Web URL of the project.
+        """
+        raise NotImplementedError()
+
+    def get_sha_from_branch(self, branch: str) -> Optional[str]:
+        """
+        Returns:
+            Commit SHA of head of the branch. `None` if no branch was found.
+        """
+        raise NotImplementedError()
+
+    def get_contributors(self) -> set[str]:
+        """
+        Returns:
+            Set of all contributors to the given project.
+        """
+        raise NotImplementedError()
+
+    def users_with_write_access(self) -> set[str]:
+        """
+        Returns:
+            List of users who have write access to the project
+        """
+        raise NotImplementedError("Use subclass instead.")
+
+    def has_write_access(self, user: str) -> bool:
+        """
+        Decides whether a given user has write access to the project.
+
+        Args:
+            user: The user we are going to check to see if he/she has access
+        """
+        return user in self.users_with_write_access()
+
+

Ancestors

+ +

Subclasses

+ +

Instance variables

+
+
var default_branch : str
+
+

Default branch (usually main, master or trunk).

+
+ +Expand source code + +
@property
+def default_branch(self) -> str:
+    """Default branch (usually `main`, `master` or `trunk`)."""
+    raise NotImplementedError()
+
+
+
var description : str
+
+

Returns

+

Project description.

+
+ +Expand source code + +
@property
+def description(self) -> str:
+    """
+    Returns:
+        Project description.
+    """
+    raise NotImplementedError()
+
+
+
var full_repo_name : str
+
+

Get repo name with namespace, e.g. rpms/python-docker-py.

+
+ +Expand source code + +
@property
+def full_repo_name(self) -> str:
+    """Get repo name with namespace, e.g. `rpms/python-docker-py`."""
+    raise NotImplementedError()
+
+
+
var has_issues : bool
+
+

True if issues are enabled on the project.

+
+ +Expand source code + +
@property
+def has_issues(self) -> bool:
+    """`True` if issues are enabled on the project."""
+    raise NotImplementedError()
+
+
+
var is_fork : bool
+
+

True if the project is a fork.

+
+ +Expand source code + +
@property
+def is_fork(self) -> bool:
+    """`True` if the project is a fork."""
+    raise NotImplementedError()
+
+
+
var parent : Optional[GitProject]
+
+

Parent project if the project is a fork, otherwise None.

+
+ +Expand source code + +
@property
+def parent(self) -> Optional["GitProject"]:
+    """Parent project if the project is a fork, otherwise `None`."""
+    raise NotImplementedError()
+
+
+
+

Methods

+
+
+def add_group(self, group: str, access_level: AccessLevel) ‑> None +
+
+

Add group to project.

+

Args

+
+
group
+
Name of the group.
+
access_level
+
Permissions for the group.
+
+
+ +Expand source code + +
def add_group(self, group: str, access_level: AccessLevel) -> None:
+    """
+    Add group to project.
+
+    Args:
+        group: Name of the group.
+        access_level: Permissions for the group.
+    """
+    raise NotImplementedError()
+
+
+
+def add_user(self, user: str, access_level: AccessLevel) ‑> None +
+
+

Add user to project.

+

Args

+
+
user
+
Username of the user.
+
access_level
+
Permissions for the user.
+
+
+ +Expand source code + +
def add_user(self, user: str, access_level: AccessLevel) -> None:
+    """
+    Add user to project.
+
+    Args:
+        user: Username of the user.
+        access_level: Permissions for the user.
+    """
+    raise NotImplementedError()
+
+
+
+def can_merge_pr(self, username: str) ‑> bool +
+
+

Args

+
+
username
+
Username.
+
+

Returns

+

True if user merge pull request, False otherwise.

+
+ +Expand source code + +
def can_merge_pr(self, username: str) -> bool:
+    """
+    Args:
+        username: Username.
+
+    Returns:
+        `True` if user merge pull request, `False` otherwise.
+    """
+    raise NotImplementedError()
+
+
+
+def change_token(self, new_token: str) ‑> None +
+
+

Change an API token. Only for the current instance.

+

Args

+
+
new_token
+
New token to be set.
+
+
+ +Expand source code + +
def change_token(self, new_token: str) -> None:
+    """
+    Change an API token. Only for the current instance.
+
+    Args:
+        new_token: New token to be set.
+    """
+    raise NotImplementedError
+
+
+
+def commit_comment(self, commit: str, body: str, filename: Optional[str] = None, row: Optional[int] = None) ‑> CommitComment +
+
+

Add new comment to a commit.

+

Args

+
+
commit
+
Hash of the commit.
+
body
+
Body of the comment.
+
filename
+
+

Name of the file that is related to the comment.

+

Defaults to None, which means no relation to file.

+
+
row
+
+

Number of the row that the comment is related to.

+

Defaults to None, which means no relation to the row.

+
+
+

Returns

+

Object that represents newly created commit comment.

+
+ +Expand source code + +
def commit_comment(
+    self,
+    commit: str,
+    body: str,
+    filename: Optional[str] = None,
+    row: Optional[int] = None,
+) -> "CommitComment":
+    """
+    Add new comment to a commit.
+
+    Args:
+        commit: Hash of the commit.
+        body: Body of the comment.
+        filename: Name of the file that is related to the comment.
+
+            Defaults to `None`, which means no relation to file.
+        row: Number of the row that the comment is related to.
+
+            Defaults to `None`, which means no relation to the row.
+
+    Returns:
+        Object that represents newly created commit comment.
+    """
+    raise NotImplementedError()
+
+
+
+def create_issue(self, title: str, body: str, private: Optional[bool] = None, labels: Optional[list[str]] = None, assignees: Optional[list[str]] = None) ‑> Issue +
+
+

Open new issue.

+

Args

+
+
title
+
Title of the issue.
+
body
+
Description of the issue.
+
private
+
+

Is the new issue supposed to be confidential?

+

Supported only by GitLab and Pagure.

+

Defaults to unset.

+
+
labels
+
+

List of labels that are to be added to +the issue.

+

Defaults to no labels.

+
+
assignees
+
+

List of usernames of the assignees.

+

Defaults to no assignees.

+
+
+

Returns

+

Object that represents newly created issue.

+

Raises

+

IssueTrackerDisabled, if issue tracker is disabled.

+
+ +Expand source code + +
def create_issue(
+    self,
+    title: str,
+    body: str,
+    private: Optional[bool] = None,
+    labels: Optional[list[str]] = None,
+    assignees: Optional[list[str]] = None,
+) -> Issue:
+    """
+    Open new issue.
+
+    Args:
+        title: Title of the issue.
+        body: Description of the issue.
+        private: Is the new issue supposed to be confidential?
+
+            **Supported only by GitLab and Pagure.**
+
+            Defaults to unset.
+        labels: List of labels that are to be added to
+            the issue.
+
+            Defaults to no labels.
+        assignees: List of usernames of the assignees.
+
+            Defaults to no assignees.
+
+    Returns:
+        Object that represents newly created issue.
+
+    Raises:
+        IssueTrackerDisabled, if issue tracker is disabled.
+    """
+    raise NotImplementedError()
+
+
+
+def create_pr(self, title: str, body: str, target_branch: str, source_branch: str, fork_username: Optional[str] = None) ‑> PullRequest +
+
+

Create new pull request.

+

Args

+
+
title
+
Title of the pull request.
+
body
+
Description of the pull request.
+
target_branch
+
Name of the branch where the changes are merged.
+
source_branch
+
Name of the branch from which the changes are pulled.
+
fork_username
+
+

The username of forked repository.

+

Defaults to None.

+
+
+

Returns

+

Object that represents newly created pull request.

+
+ +Expand source code + +
def create_pr(
+    self,
+    title: str,
+    body: str,
+    target_branch: str,
+    source_branch: str,
+    fork_username: Optional[str] = None,
+) -> "PullRequest":
+    """
+    Create new pull request.
+
+    Args:
+        title: Title of the pull request.
+        body: Description of the pull request.
+        target_branch: Name of the branch where the changes are merged.
+        source_branch: Name of the branch from which the changes are pulled.
+        fork_username: The username of forked repository.
+
+            Defaults to `None`.
+
+    Returns:
+        Object that represents newly created pull request.
+    """
+    raise NotImplementedError()
+
+
+
+def create_release(self, tag: str, name: str, message: str, ref: Optional[str] = None) ‑> Release +
+
+

Create new release.

+

Args

+
+
tag
+
Tag which is the release based off.
+
name
+
Name of the release.
+
message
+
Message or description of the release.
+
ref
+
+

Git reference, mainly commit hash for the release. If provided +git tag is created prior to creating a release.

+

Defaults to None.

+
+
+

Returns

+

Object that represents newly created release.

+
+ +Expand source code + +
def create_release(
+    self,
+    tag: str,
+    name: str,
+    message: str,
+    ref: Optional[str] = None,
+) -> Release:
+    """
+    Create new release.
+
+    Args:
+        tag: Tag which is the release based off.
+        name: Name of the release.
+        message: Message or description of the release.
+        ref: Git reference, mainly commit hash for the release. If provided
+            git tag is created prior to creating a release.
+
+            Defaults to `None`.
+
+    Returns:
+        Object that represents newly created release.
+    """
+    raise NotImplementedError()
+
+
+
+def delete(self) ‑> None +
+
+

Delete the project.

+
+ +Expand source code + +
def delete(self) -> None:
+    """Delete the project."""
+    raise NotImplementedError()
+
+
+
+def exists(self) ‑> bool +
+
+

Check the existence of the repo.

+

Returns

+

True if the project exists, False otherwise.

+
+ +Expand source code + +
def exists(self) -> bool:
+    """
+    Check the existence of the repo.
+
+    Returns:
+        `True` if the project exists, `False` otherwise.
+    """
+    raise NotImplementedError()
+
+
+
+def fork_create(self, namespace: Optional[str] = None) ‑> GitProject +
+
+

Fork this project using the authenticated user.

+

Args

+
+
namespace
+
+

Namespace where the project should be forked.

+

Defaults to None, which means forking to the namespace of +currently authenticated user.

+
+
+

Returns

+

Fork of the current project.

+

Raises

+

In case the fork already exists.

+
+ +Expand source code + +
def fork_create(self, namespace: Optional[str] = None) -> "GitProject":
+    """
+    Fork this project using the authenticated user.
+
+    Args:
+        namespace: Namespace where the project should be forked.
+
+            Defaults to `None`, which means forking to the namespace of
+            currently authenticated user.
+
+    Returns:
+        Fork of the current project.
+
+    Raises:
+        In case the fork already exists.
+    """
+    raise NotImplementedError()
+
+
+
+def get_branches(self) ‑> list[str] +
+
+

Returns

+

List with names of branches in the project.

+
+ +Expand source code + +
def get_branches(self) -> list[str]:
+    """
+    Returns:
+        List with names of branches in the project.
+    """
+    raise NotImplementedError()
+
+
+
+def get_commit_comments(self, commit: str) ‑> list[CommitComment] +
+
+

Get comments for a commit.

+

Args

+
+
commit
+
The hash of the commit.
+
+

Returns

+

List of all comments for the commit.

+
+ +Expand source code + +
def get_commit_comments(self, commit: str) -> list[CommitComment]:
+    """
+    Get comments for a commit.
+
+    Args:
+        commit: The hash of the commit.
+
+    Returns:
+        List of all comments for the commit.
+    """
+    raise NotImplementedError()
+
+
+
+def get_commit_statuses(self, commit: str) ‑> list[CommitFlag] +
+
+

Get statuses of the commit.

+

Args

+
+
commit
+
Hash of the commit.
+
+

Returns

+

List of all commit statuses on the commit.

+
+ +Expand source code + +
def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+    """
+    Get statuses of the commit.
+
+    Args:
+        commit: Hash of the commit.
+
+    Returns:
+        List of all commit statuses on the commit.
+    """
+    raise NotImplementedError()
+
+
+
+def get_contributors(self) ‑> set[str] +
+
+

Returns

+

Set of all contributors to the given project.

+
+ +Expand source code + +
def get_contributors(self) -> set[str]:
+    """
+    Returns:
+        Set of all contributors to the given project.
+    """
+    raise NotImplementedError()
+
+
+
+def get_description(self) ‑> str +
+
+

Returns

+

Project description.

+
+ +Expand source code + +
def get_description(self) -> str:
+    """
+    Returns:
+        Project description.
+    """
+    raise NotImplementedError()
+
+
+
+def get_file_content(self, path: str, ref: Optional[str] = None) ‑> str +
+
+

Get a content of the file in the repo.

+

Args

+
+
path
+
Path to the file.
+
ref
+
+

Branch or commit.

+

Defaults to repo's default branch.

+
+
+

Returns

+

Contents of the file as string.

+

Raises

+
+
FileNotFoundError
+
if there is no such file.
+
+
+ +Expand source code + +
def get_file_content(self, path: str, ref: Optional[str] = None) -> str:
+    """
+    Get a content of the file in the repo.
+
+    Args:
+        path: Path to the file.
+        ref: Branch or commit.
+
+            Defaults to repo's default branch.
+
+    Returns:
+        Contents of the file as string.
+
+    Raises:
+        FileNotFoundError: if there is no such file.
+    """
+    raise NotImplementedError
+
+
+
+def get_files(self, ref: Optional[str] = None, filter_regex: Optional[str] = None, recursive: bool = False) ‑> list[str] +
+
+

Get a list of file paths of the repo.

+

Args

+
+
ref
+
+

Branch or commit.

+

Defaults to repo's default branch.

+
+
filter_regex
+
+

Filter the paths with re.search.

+

Defaults to None, which means no filtering.

+
+
recursive
+
+

Whether to return only top directory files +or all files recursively.

+

Defaults to False, which means only top-level directory.

+
+
+

Returns

+

List of paths of the files in the repo.

+
+ +Expand source code + +
def get_files(
+    self,
+    ref: Optional[str] = None,
+    filter_regex: Optional[str] = None,
+    recursive: bool = False,
+) -> list[str]:
+    """
+    Get a list of file paths of the repo.
+
+    Args:
+        ref: Branch or commit.
+
+            Defaults to repo's default branch.
+        filter_regex: Filter the paths with `re.search`.
+
+            Defaults to `None`, which means no filtering.
+        recursive: Whether to return only top directory files
+            or all files recursively.
+
+            Defaults to `False`, which means only top-level directory.
+
+    Returns:
+        List of paths of the files in the repo.
+    """
+    raise NotImplementedError
+
+
+
+def get_fork(self, create: bool = True) ‑> Optional[GitProject] +
+
+

Provide GitProject instance of a fork of this project.

+

Args

+
+
create
+
Create fork if it does not exist.
+
+

Returns

+

None if the project is fork itself or there is no fork, otherwise +instance of a fork if is to be created or exists already.

+
+ +Expand source code + +
def get_fork(self, create: bool = True) -> Optional["GitProject"]:
+    """
+    Provide GitProject instance of a fork of this project.
+
+    Args:
+        create: Create fork if it does not exist.
+
+    Returns:
+        `None` if the project is fork itself or there is no fork, otherwise
+        instance of a fork if is to be created or exists already.
+    """
+    raise NotImplementedError()
+
+
+
+def get_forks(self) ‑> collections.abc.Sequence['GitProject'] +
+
+

Returns

+

All forks of the project.

+
+ +Expand source code + +
def get_forks(self) -> Sequence["GitProject"]:
+    """
+    Returns:
+        All forks of the project.
+    """
+    raise NotImplementedError()
+
+
+
+def get_git_urls(self) ‑> dict[str, str] +
+
+

Get git URLs for the project.

+

Returns

+

Dictionary with at least SSH and HTTP URLs for the current project.

+
+ +Expand source code + +
def get_git_urls(self) -> dict[str, str]:
+    """
+    Get git URLs for the project.
+
+    Returns:
+        Dictionary with at least SSH and HTTP URLs for the current project.
+    """
+    raise NotImplementedError()
+
+
+
+def get_issue(self, issue_id: int) ‑> Issue +
+
+

Get issue.

+

Args

+
+
issue_id
+
ID of the issue.
+
+

Returns

+

Object that represents requested issue.

+
+ +Expand source code + +
def get_issue(self, issue_id: int) -> "Issue":
+    """
+    Get issue.
+
+    Args:
+        issue_id: ID of the issue.
+
+    Returns:
+        Object that represents requested issue.
+    """
+    raise NotImplementedError()
+
+
+
+def get_issue_info(self, issue_id: int) ‑> Issue +
+
+

Get issue info.

+

Args

+
+
issue_id
+
ID of the issue.
+
+

Returns

+

Object that represents requested issue.

+
+ +Expand source code + +
def get_issue_info(self, issue_id: int) -> "Issue":
+    """
+    Get issue info.
+
+    Args:
+        issue_id: ID of the issue.
+
+    Returns:
+        Object that represents requested issue.
+    """
+    raise NotImplementedError()
+
+
+
+def get_issue_list(self, status: IssueStatus = IssueStatus.open, author: Optional[str] = None, assignee: Optional[str] = None, labels: Optional[list[str]] = None) ‑> list['Issue'] +
+
+

List of issues.

+

Args

+
+
status
+
+

Status of the issues that are to be +included in the list.

+

Defaults to IssueStatus.open.

+
+
author
+
+

Username of the author of the issues.

+

Defaults to no filtering by author.

+
+
assignee
+
+

Username of the assignee on the issues.

+

Defaults to no filtering by assignees.

+
+
labels
+
+

Filter issues that have set specific labels.

+

Defaults to no filtering by labels.

+
+
+

Returns

+

List of objects that represent requested issues.

+
+ +Expand source code + +
def get_issue_list(
+    self,
+    status: IssueStatus = IssueStatus.open,
+    author: Optional[str] = None,
+    assignee: Optional[str] = None,
+    labels: Optional[list[str]] = None,
+) -> list["Issue"]:
+    """
+    List of issues.
+
+    Args:
+        status: Status of the issues that are to be
+            included in the list.
+
+            Defaults to `IssueStatus.open`.
+        author: Username of the author of the issues.
+
+            Defaults to no filtering by author.
+        assignee: Username of the assignee on the issues.
+
+            Defaults to no filtering by assignees.
+        labels: Filter issues that have set specific labels.
+
+            Defaults to no filtering by labels.
+
+    Returns:
+        List of objects that represent requested issues.
+    """
+    raise NotImplementedError()
+
+
+
+def get_latest_release(self) ‑> Optional[Release] +
+
+

Returns

+

Object that represents the latest release.

+
+ +Expand source code + +
def get_latest_release(self) -> Optional[Release]:
+    """
+    Returns:
+        Object that represents the latest release.
+    """
+    raise NotImplementedError()
+
+
+
+def get_owners(self) ‑> list[str] +
+
+

Returns

+

List of usernames of project owners.

+
+ +Expand source code + +
def get_owners(self) -> list[str]:
+    """
+    Returns:
+        List of usernames of project owners.
+    """
+    raise NotImplementedError()
+
+
+
+def get_pr(self, pr_id: int) ‑> PullRequest +
+
+

Get pull request.

+

Args

+
+
pr_id
+
ID of the pull request.
+
+

Returns

+

Object that represents requested pull request.

+
+ +Expand source code + +
def get_pr(self, pr_id: int) -> "PullRequest":
+    """
+    Get pull request.
+
+    Args:
+        pr_id: ID of the pull request.
+
+    Returns:
+        Object that represents requested pull request.
+    """
+    raise NotImplementedError()
+
+
+
+def get_pr_files_diff(self, pr_id: int, retries: int = 0, wait_seconds: int = 3) ‑> dict +
+
+

Get files diff of a pull request.

+

Args

+
+
pr_id
+
ID of the pull request.
+
+

Returns

+

Dictionary representing files diff.

+
+ +Expand source code + +
def get_pr_files_diff(
+    self,
+    pr_id: int,
+    retries: int = 0,
+    wait_seconds: int = 3,
+) -> dict:
+    """
+    Get files diff of a pull request.
+
+    Args:
+        pr_id: ID of the pull request.
+
+    Returns:
+        Dictionary representing files diff.
+    """
+    raise NotImplementedError()
+
+
+
+def get_pr_list(self, status: PRStatus = PRStatus.open) ‑> list['PullRequest'] +
+
+

List of pull requests.

+

Args

+
+
status
+
+

Status of the pull requests that are to be included in the list.

+

Defaults to PRStatus.open.

+
+
+

Returns

+

List of objects that represent pull requests with requested status.

+
+ +Expand source code + +
def get_pr_list(self, status: PRStatus = PRStatus.open) -> list["PullRequest"]:
+    """
+    List of pull requests.
+
+    Args:
+        status: Status of the pull requests that are to be included in the list.
+
+            Defaults to `PRStatus.open`.
+
+    Returns:
+        List of objects that represent pull requests with requested status.
+    """
+    raise NotImplementedError()
+
+
+
+def get_release(self, identifier: Optional[int] = None, name: Optional[str] = None, tag_name: Optional[str] = None) ‑> Release +
+
+

Get a single release.

+

Args

+
+
identifier
+
+

Identifier of the release.

+

Defaults to None, which means not being used.

+
+
name
+
+

Name of the release.

+

Defaults to None, which means not being used.

+
+
tag_name
+
+

Tag that the release is tied to.

+

Defaults to None, which means not being used.

+
+
+

Returns

+

Object that represents release that satisfies requested condition.

+
+ +Expand source code + +
def get_release(
+    self,
+    identifier: Optional[int] = None,
+    name: Optional[str] = None,
+    tag_name: Optional[str] = None,
+) -> Release:
+    """
+    Get a single release.
+
+    Args:
+        identifier: Identifier of the release.
+
+            Defaults to `None`, which means not being used.
+        name: Name of the release.
+
+            Defaults to `None`, which means not being used.
+        tag_name: Tag that the release is tied to.
+
+            Defaults to `None`, which means not being used.
+
+    Returns:
+        Object that represents release that satisfies requested condition.
+    """
+    raise NotImplementedError()
+
+
+
+def get_releases(self) ‑> list[Release] +
+
+

Returns

+

List of the objects that represent releases.

+
+ +Expand source code + +
def get_releases(self) -> list[Release]:
+    """
+    Returns:
+        List of the objects that represent releases.
+    """
+    raise NotImplementedError()
+
+
+
+def get_sha_from_branch(self, branch: str) ‑> Optional[str] +
+
+

Returns

+

Commit SHA of head of the branch. None if no branch was found.

+
+ +Expand source code + +
def get_sha_from_branch(self, branch: str) -> Optional[str]:
+    """
+    Returns:
+        Commit SHA of head of the branch. `None` if no branch was found.
+    """
+    raise NotImplementedError()
+
+
+
+def get_sha_from_tag(self, tag_name: str) ‑> str +
+
+

Args

+
+
tag_name
+
Name of the tag.
+
+

Returns

+

Commit hash of the commit from the requested tag.

+
+ +Expand source code + +
def get_sha_from_tag(self, tag_name: str) -> str:
+    """
+    Args:
+        tag_name: Name of the tag.
+
+    Returns:
+        Commit hash of the commit from the requested tag.
+    """
+    raise NotImplementedError()
+
+
+
+def get_tags(self) ‑> list['GitTag'] +
+
+

Returns

+

List of objects that represent tags.

+
+ +Expand source code + +
def get_tags(self) -> list["GitTag"]:
+    """
+    Returns:
+        List of objects that represent tags.
+    """
+    raise NotImplementedError()
+
+
+
+def get_web_url(self) ‑> str +
+
+

Returns

+

Web URL of the project.

+
+ +Expand source code + +
def get_web_url(self) -> str:
+    """
+    Returns:
+        Web URL of the project.
+    """
+    raise NotImplementedError()
+
+
+
+def has_write_access(self, user: str) ‑> bool +
+
+

Decides whether a given user has write access to the project.

+

Args

+
+
user
+
The user we are going to check to see if he/she has access
+
+
+ +Expand source code + +
def has_write_access(self, user: str) -> bool:
+    """
+    Decides whether a given user has write access to the project.
+
+    Args:
+        user: The user we are going to check to see if he/she has access
+    """
+    return user in self.users_with_write_access()
+
+
+
+def is_forked(self) ‑> bool +
+
+

Is this repository forked by the authenticated user?

+

Returns

+

True, if the repository is fork.

+
+ +Expand source code + +
def is_forked(self) -> bool:
+    """
+    Is this repository forked by the authenticated user?
+
+    Returns:
+        `True`, if the repository is fork.
+    """
+    raise NotImplementedError()
+
+
+
+def is_private(self) ‑> bool +
+
+

Is this repository private (accessible only by users with permissions).

+

Returns

+

True, if the repository is private.

+
+ +Expand source code + +
def is_private(self) -> bool:
+    """
+    Is this repository private (accessible only by users with permissions).
+
+    Returns:
+        `True`, if the repository is private.
+    """
+    raise NotImplementedError()
+
+
+
+def remove_group(self, group: str) ‑> None +
+
+

Remove group from project.

+

Args

+
+
group
+
Name of the group.
+
+
+ +Expand source code + +
def remove_group(self, group: str) -> None:
+    """
+    Remove group from project.
+
+    Args:
+        group: Name of the group.
+    """
+    raise NotImplementedError()
+
+
+
+def remove_user(self, user: str) ‑> None +
+
+

Remove user from project.

+

Args

+
+
user
+
Username of the user.
+
+
+ +Expand source code + +
def remove_user(self, user: str) -> None:
+    """
+    Remove user from project.
+
+    Args:
+        user: Username of the user.
+    """
+    raise NotImplementedError()
+
+
+
+def request_access(self) ‑> None +
+
+

Request an access to the project (cannot specify access level to be granted; +needs to be approved and specified by the user with maintainer/admin rights).

+
+ +Expand source code + +
def request_access(self) -> None:
+    """
+    Request an access to the project (cannot specify access level to be granted;
+    needs to be approved and specified by the user with maintainer/admin rights).
+    """
+    raise NotImplementedError()
+
+
+
+def set_commit_status(self, commit: str, state: Union[CommitStatus, str], target_url: str, description: str, context: str, trim: bool = False) ‑> CommitFlag +
+
+

Create a status on a commit.

+

Args

+
+
commit
+
The hash of the commit.
+
state
+
The state of the status.
+
target_url
+
The target URL to associate with this status.
+
description
+
A short description of the status.
+
context
+
A label to differentiate this status from the status of other systems.
+
trim
+
+

Whether to trim the description to 140 characters.

+

Defaults to False.

+
+
+

Returns

+

Object that represents created commit status.

+
+ +Expand source code + +
def set_commit_status(
+    self,
+    commit: str,
+    state: Union[CommitStatus, str],
+    target_url: str,
+    description: str,
+    context: str,
+    trim: bool = False,
+) -> "CommitFlag":
+    """
+    Create a status on a commit.
+
+    Args:
+        commit: The hash of the commit.
+        state: The state of the status.
+        target_url: The target URL to associate with this status.
+        description: A short description of the status.
+        context: A label to differentiate this status from the status of other systems.
+        trim: Whether to trim the description to 140 characters.
+
+            Defaults to `False`.
+
+    Returns:
+        Object that represents created commit status.
+    """
+    raise NotImplementedError()
+
+
+
+def users_with_write_access(self) ‑> set[str] +
+
+

Returns

+

List of users who have write access to the project

+
+ +Expand source code + +
def users_with_write_access(self) -> set[str]:
+    """
+    Returns:
+        List of users who have write access to the project
+    """
+    raise NotImplementedError("Use subclass instead.")
+
+
+
+def which_groups_can_merge_pr(self) ‑> set[str] +
+
+

Returns

+

Names of all groups that have permission to modify pull request.

+
+ +Expand source code + +
def which_groups_can_merge_pr(self) -> set[str]:
+    """
+    Returns:
+        Names of all groups that have permission to modify pull request.
+    """
+    raise NotImplementedError()
+
+
+
+def who_can_close_issue(self) ‑> set[str] +
+
+

Returns

+

Names of all users who have permission to modify an issue.

+
+ +Expand source code + +
def who_can_close_issue(self) -> set[str]:
+    """
+    Returns:
+        Names of all users who have permission to modify an issue.
+    """
+    raise NotImplementedError()
+
+
+
+def who_can_merge_pr(self) ‑> set[str] +
+
+

Returns

+

Names of all users who have permission to modify pull request.

+
+ +Expand source code + +
def who_can_merge_pr(self) -> set[str]:
+    """
+    Returns:
+        Names of all users who have permission to modify pull request.
+    """
+    raise NotImplementedError()
+
+
+
+
+
+class GitService +(**_: Any) +
+
+

Attributes

+
+
instance_url : str
+
URL of the git forge instance.
+
+
+ +Expand source code + +
class GitService(OgrAbstractClass):
+    """
+    Attributes:
+        instance_url (str): URL of the git forge instance.
+    """
+
+    instance_url: Optional[str] = None
+
+    def __init__(self, **_: Any) -> None:
+        pass
+
+    def __str__(self) -> str:
+        return f"GitService(instance_url={self.instance_url})"
+
+    def get_project(self, **kwargs: Any) -> "GitProject":
+        """
+        Get the requested project.
+
+        Args:
+            namespace (str): Namespace of the project.
+            user (str): Username of the project's owner.
+            repo (str): Repository name.
+
+        Returns:
+            Object that represents git project.
+        """
+        raise NotImplementedError
+
+    def get_project_from_url(self, url: str) -> "GitProject":
+        """
+        Args:
+            url: URL of the git repository.
+
+        Returns:
+            Object that represents project from the parsed URL.
+        """
+        repo_url = parse_git_repo(potential_url=url)
+        if not repo_url:
+            raise OgrException(f"Failed to find repository for url: {url}")
+        return self.get_project(repo=repo_url.repo, namespace=repo_url.namespace)
+
+    @_cached_property
+    def hostname(self) -> Optional[str]:
+        """Hostname of the service."""
+        raise NotImplementedError
+
+    @property
+    def user(self) -> "GitUser":
+        """User authenticated through the service."""
+        raise NotImplementedError
+
+    def change_token(self, new_token: str) -> None:
+        """
+        Change an API token. Only for the current instance and newly created projects.
+
+        Args:
+            new_token: New token to be set.
+        """
+        raise NotImplementedError
+
+    def set_auth_method(self, method: AuthMethod) -> None:
+        """
+        Override the default auth method.
+        Can be used when the service has more auth methods available.
+
+        Args:
+            method: the method identifier (a str name)
+        """
+        raise NotImplementedError()
+
+    def reset_auth_method(self) -> None:
+        """
+        Set the auth method to the default one.
+        """
+        raise NotImplementedError()
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "GitProject":
+        """
+        Create new project.
+
+        Args:
+            repo: Name of the newly created project.
+            namespace: Namespace of the newly created project.
+
+                Defaults to currently authenticated user.
+            description: Description of the newly created project.
+
+        Returns:
+            Object that represents newly created project.
+        """
+        raise NotImplementedError()
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list["GitProject"]:
+        """
+        List projects for given criteria.
+
+        Args:
+            namespace: Namespace to list projects from.
+            user: Login of the owner of the projects.
+            search_pattern: Regular expression that repository name should match.
+            language: Language to be present in the project, e.g. `"python"` or
+                `"html"`.
+        """
+        raise NotImplementedError
+
+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var instance_url : Optional[str]
+
+
+
+
+

Instance variables

+
+
var hostname
+
+

Hostname of the service.

+
+ +Expand source code + +
def __get__(self, instance, owner=None):
+    if instance is None:
+        return self
+    if self.attrname is None:
+        raise TypeError(
+            "Cannot use cached_property instance without calling __set_name__ on it.")
+    try:
+        cache = instance.__dict__
+    except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
+        msg = (
+            f"No '__dict__' attribute on {type(instance).__name__!r} "
+            f"instance to cache {self.attrname!r} property."
+        )
+        raise TypeError(msg) from None
+    val = cache.get(self.attrname, _NOT_FOUND)
+    if val is _NOT_FOUND:
+        with self.lock:
+            # check if another thread filled cache while we awaited lock
+            val = cache.get(self.attrname, _NOT_FOUND)
+            if val is _NOT_FOUND:
+                val = self.func(instance)
+                try:
+                    cache[self.attrname] = val
+                except TypeError:
+                    msg = (
+                        f"The '__dict__' attribute on {type(instance).__name__!r} instance "
+                        f"does not support item assignment for caching {self.attrname!r} property."
+                    )
+                    raise TypeError(msg) from None
+    return val
+
+
+
var userGitUser
+
+

User authenticated through the service.

+
+ +Expand source code + +
@property
+def user(self) -> "GitUser":
+    """User authenticated through the service."""
+    raise NotImplementedError
+
+
+
+

Methods

+
+
+def change_token(self, new_token: str) ‑> None +
+
+

Change an API token. Only for the current instance and newly created projects.

+

Args

+
+
new_token
+
New token to be set.
+
+
+ +Expand source code + +
def change_token(self, new_token: str) -> None:
+    """
+    Change an API token. Only for the current instance and newly created projects.
+
+    Args:
+        new_token: New token to be set.
+    """
+    raise NotImplementedError
+
+
+
+def get_project(self, **kwargs: Any) ‑> GitProject +
+
+

Get the requested project.

+

Args

+
+
namespace : str
+
Namespace of the project.
+
user : str
+
Username of the project's owner.
+
repo : str
+
Repository name.
+
+

Returns

+

Object that represents git project.

+
+ +Expand source code + +
def get_project(self, **kwargs: Any) -> "GitProject":
+    """
+    Get the requested project.
+
+    Args:
+        namespace (str): Namespace of the project.
+        user (str): Username of the project's owner.
+        repo (str): Repository name.
+
+    Returns:
+        Object that represents git project.
+    """
+    raise NotImplementedError
+
+
+
+def get_project_from_url(self, url: str) ‑> GitProject +
+
+

Args

+
+
url
+
URL of the git repository.
+
+

Returns

+

Object that represents project from the parsed URL.

+
+ +Expand source code + +
def get_project_from_url(self, url: str) -> "GitProject":
+    """
+    Args:
+        url: URL of the git repository.
+
+    Returns:
+        Object that represents project from the parsed URL.
+    """
+    repo_url = parse_git_repo(potential_url=url)
+    if not repo_url:
+        raise OgrException(f"Failed to find repository for url: {url}")
+    return self.get_project(repo=repo_url.repo, namespace=repo_url.namespace)
+
+
+
+def list_projects(self, namespace: Optional[str] = None, user: Optional[str] = None, search_pattern: Optional[str] = None, language: Optional[str] = None) ‑> list['GitProject'] +
+
+

List projects for given criteria.

+

Args

+
+
namespace
+
Namespace to list projects from.
+
user
+
Login of the owner of the projects.
+
search_pattern
+
Regular expression that repository name should match.
+
language
+
Language to be present in the project, e.g. "python" or +"html".
+
+
+ +Expand source code + +
def list_projects(
+    self,
+    namespace: Optional[str] = None,
+    user: Optional[str] = None,
+    search_pattern: Optional[str] = None,
+    language: Optional[str] = None,
+) -> list["GitProject"]:
+    """
+    List projects for given criteria.
+
+    Args:
+        namespace: Namespace to list projects from.
+        user: Login of the owner of the projects.
+        search_pattern: Regular expression that repository name should match.
+        language: Language to be present in the project, e.g. `"python"` or
+            `"html"`.
+    """
+    raise NotImplementedError
+
+
+
+def project_create(self, repo: str, namespace: Optional[str] = None, description: Optional[str] = None) ‑> GitProject +
+
+

Create new project.

+

Args

+
+
repo
+
Name of the newly created project.
+
namespace
+
+

Namespace of the newly created project.

+

Defaults to currently authenticated user.

+
+
description
+
Description of the newly created project.
+
+

Returns

+

Object that represents newly created project.

+
+ +Expand source code + +
def project_create(
+    self,
+    repo: str,
+    namespace: Optional[str] = None,
+    description: Optional[str] = None,
+) -> "GitProject":
+    """
+    Create new project.
+
+    Args:
+        repo: Name of the newly created project.
+        namespace: Namespace of the newly created project.
+
+            Defaults to currently authenticated user.
+        description: Description of the newly created project.
+
+    Returns:
+        Object that represents newly created project.
+    """
+    raise NotImplementedError()
+
+
+
+def reset_auth_method(self) ‑> None +
+
+

Set the auth method to the default one.

+
+ +Expand source code + +
def reset_auth_method(self) -> None:
+    """
+    Set the auth method to the default one.
+    """
+    raise NotImplementedError()
+
+
+
+def set_auth_method(self, method: AuthMethod) ‑> None +
+
+

Override the default auth method. +Can be used when the service has more auth methods available.

+

Args

+
+
method
+
the method identifier (a str name)
+
+
+ +Expand source code + +
def set_auth_method(self, method: AuthMethod) -> None:
+    """
+    Override the default auth method.
+    Can be used when the service has more auth methods available.
+
+    Args:
+        method: the method identifier (a str name)
+    """
+    raise NotImplementedError()
+
+
+
+
+
+class GitTag +(name: str, commit_sha: str) +
+
+

Class representing a git tag.

+

Attributes

+
+
name : str
+
Name of the tag.
+
commit_sha : str
+
Commit hash of the tag.
+
+
+ +Expand source code + +
class GitTag(OgrAbstractClass):
+    """
+    Class representing a git tag.
+
+    Attributes:
+        name (str): Name of the tag.
+        commit_sha (str): Commit hash of the tag.
+    """
+
+    def __init__(self, name: str, commit_sha: str) -> None:
+        self.name = name
+        self.commit_sha = commit_sha
+
+    def __str__(self) -> str:
+        return f"GitTag(name={self.name}, commit_sha={self.commit_sha})"
+
+

Ancestors

+ +
+
+class GitUser +(service: GitService) +
+
+

Represents currently authenticated user through service.

+
+ +Expand source code + +
class GitUser(OgrAbstractClass):
+    """
+    Represents currently authenticated user through service.
+    """
+
+    def __init__(self, service: GitService) -> None:
+        self.service = service
+
+    def get_username(self) -> str:
+        """
+        Returns:
+            Login of the user.
+        """
+        raise NotImplementedError()
+
+    def get_email(self) -> str:
+        """
+        Returns:
+            Email of the user.
+        """
+        raise NotImplementedError()
+
+    def get_projects(self) -> Sequence["GitProject"]:
+        """
+        Returns:
+            Sequence of projects in user's namespace.
+        """
+        raise NotImplementedError()
+
+    def get_forks(self) -> Sequence["GitProject"]:
+        """
+        Returns:
+            Sequence of forks in user's namespace.
+        """
+        raise NotImplementedError()
+
+

Ancestors

+ +

Subclasses

+ +

Methods

+
+
+def get_email(self) ‑> str +
+
+

Returns

+

Email of the user.

+
+ +Expand source code + +
def get_email(self) -> str:
+    """
+    Returns:
+        Email of the user.
+    """
+    raise NotImplementedError()
+
+
+
+def get_forks(self) ‑> collections.abc.Sequence['GitProject'] +
+
+

Returns

+

Sequence of forks in user's namespace.

+
+ +Expand source code + +
def get_forks(self) -> Sequence["GitProject"]:
+    """
+    Returns:
+        Sequence of forks in user's namespace.
+    """
+    raise NotImplementedError()
+
+
+
+def get_projects(self) ‑> collections.abc.Sequence['GitProject'] +
+
+

Returns

+

Sequence of projects in user's namespace.

+
+ +Expand source code + +
def get_projects(self) -> Sequence["GitProject"]:
+    """
+    Returns:
+        Sequence of projects in user's namespace.
+    """
+    raise NotImplementedError()
+
+
+
+def get_username(self) ‑> str +
+
+

Returns

+

Login of the user.

+
+ +Expand source code + +
def get_username(self) -> str:
+    """
+    Returns:
+        Login of the user.
+    """
+    raise NotImplementedError()
+
+
+
+
+
+class Issue +(raw_issue: Any, project: GitProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the issue.
+
+
+ +Expand source code + +
class Issue(OgrAbstractClass):
+    """
+    Attributes:
+        project (GitProject): Project of the issue.
+    """
+
+    def __init__(self, raw_issue: Any, project: "GitProject") -> None:
+        self._raw_issue = raw_issue
+        self.project = project
+
+    @property
+    def title(self) -> str:
+        """Title of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def private(self) -> bool:
+        """`True` if issue is confidential, `False` otherwise."""
+        raise NotImplementedError()
+
+    @property
+    def id(self) -> int:
+        """ID of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def status(self) -> IssueStatus:
+        """Status of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def url(self) -> str:
+        """Web URL of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def description(self) -> str:
+        """Description of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def author(self) -> str:
+        """Username of the author of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def created(self) -> datetime.datetime:
+        """Datetime of the creation of the issue."""
+        raise NotImplementedError()
+
+    @property
+    def labels(self) -> list:
+        """Labels of the issue."""
+        raise NotImplementedError()
+
+    def __str__(self) -> str:
+        description = (
+            f"{self.description[:10]}..." if self.description is not None else "None"
+        )
+        return (
+            f"Issue("
+            f"title='{self.title}', "
+            f"id={self.id}, "
+            f"status='{self.status.name}', "
+            f"url='{self.url}', "
+            f"description='{description}', "
+            f"author='{self.author}', "
+            f"created='{self.created}')"
+        )
+
+    @staticmethod
+    def create(
+        project: Any,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> "Issue":
+        """
+        Open new issue.
+
+        Args:
+            project (GitProject): Project where the issue is to be opened.
+            title: Title of the issue.
+            body: Description of the issue.
+            private: Is the new issue supposed to be confidential?
+
+                **Supported only by GitLab and Pagure.**
+
+                Defaults to unset.
+            labels: List of labels that are to be added to
+                the issue.
+
+                Defaults to no labels.
+            assignees: List of usernames of the assignees.
+
+                Defaults to no assignees.
+
+        Returns:
+            Object that represents newly created issue.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get(project: Any, id: int) -> "Issue":
+        """
+        Get issue.
+
+        Args:
+            project (GitProject): Project where the issue is to be opened.
+            issue_id: ID of the issue.
+
+        Returns:
+            Object that represents requested issue.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get_list(
+        project: Any,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        """
+        List of issues.
+
+        Args:
+            project (GitProject): Project where the issue is to be opened.
+            status: Status of the issues that are to be
+                included in the list.
+
+                Defaults to `IssueStatus.open`.
+            author: Username of the author of the issues.
+
+                Defaults to no filtering by author.
+            assignee: Username of the assignee on the issues.
+
+                Defaults to no filtering by assignees.
+            labels: Filter issues that have set specific labels.
+
+                Defaults to no filtering by labels.
+
+        Returns:
+            List of objects that represent requested issues.
+        """
+        raise NotImplementedError()
+
+    def _get_all_comments(self) -> list[IssueComment]:
+        """
+        Get list of all issue comments.
+
+        Returns:
+            List of all comments on the issue.
+        """
+        raise NotImplementedError()
+
+    def get_comments(
+        self,
+        filter_regex: Optional[str] = None,
+        reverse: bool = False,
+        author: Optional[str] = None,
+    ) -> list[IssueComment]:
+        """
+        Get list of issue comments.
+
+        Args:
+            filter_regex: Filter the comments' content with `re.search`.
+
+                Defaults to `None`, which means no filtering.
+            reverse: Whether the comments are to be returned in
+                reversed order.
+
+                Defaults to `False`.
+            author: Filter the comments by author.
+
+                Defaults to `None`, which means no filtering.
+
+        Returns:
+            List of issue comments.
+        """
+        raise NotImplementedError()
+
+    def can_close(self, username: str) -> bool:
+        """
+        Check if user have permissions to modify an issue.
+
+        Args:
+            username: Login of the user.
+
+        Returns:
+            `True` if user can close the issue, `False` otherwise.
+        """
+        raise NotImplementedError()
+
+    def comment(self, body: str) -> IssueComment:
+        """
+        Add new comment to the issue.
+
+        Args:
+            body: Text contents of the comment.
+
+        Returns:
+            Object that represents posted comment.
+        """
+        raise NotImplementedError()
+
+    def close(self) -> "Issue":
+        """
+        Close an issue.
+
+        Returns:
+            Issue itself.
+        """
+        raise NotImplementedError()
+
+    def add_label(self, *labels: str) -> None:
+        """
+        Add labels to the issue.
+
+        Args:
+            *labels: Labels to be added.
+        """
+        raise NotImplementedError()
+
+    def add_assignee(self, *assignees: str) -> None:
+        """
+        Assign users to an issue.
+
+        Args:
+            *assignees: List of logins of the assignees.
+        """
+        raise NotImplementedError()
+
+    def get_comment(self, comment_id: int) -> IssueComment:
+        """
+        Returns an issue comment.
+
+        Args:
+            comment_id: id of a comment
+
+        Returns:
+            Object representing an issue comment.
+        """
+        raise NotImplementedError()
+
+

Ancestors

+ +

Subclasses

+ +

Static methods

+
+
+def create(project: Any, title: str, body: str, private: Optional[bool] = None, labels: Optional[list[str]] = None, assignees: Optional[list[str]] = None) ‑> Issue +
+
+

Open new issue.

+

Args

+
+
project : GitProject
+
Project where the issue is to be opened.
+
title
+
Title of the issue.
+
body
+
Description of the issue.
+
private
+
+

Is the new issue supposed to be confidential?

+

Supported only by GitLab and Pagure.

+

Defaults to unset.

+
+
labels
+
+

List of labels that are to be added to +the issue.

+

Defaults to no labels.

+
+
assignees
+
+

List of usernames of the assignees.

+

Defaults to no assignees.

+
+
+

Returns

+

Object that represents newly created issue.

+
+ +Expand source code + +
@staticmethod
+def create(
+    project: Any,
+    title: str,
+    body: str,
+    private: Optional[bool] = None,
+    labels: Optional[list[str]] = None,
+    assignees: Optional[list[str]] = None,
+) -> "Issue":
+    """
+    Open new issue.
+
+    Args:
+        project (GitProject): Project where the issue is to be opened.
+        title: Title of the issue.
+        body: Description of the issue.
+        private: Is the new issue supposed to be confidential?
+
+            **Supported only by GitLab and Pagure.**
+
+            Defaults to unset.
+        labels: List of labels that are to be added to
+            the issue.
+
+            Defaults to no labels.
+        assignees: List of usernames of the assignees.
+
+            Defaults to no assignees.
+
+    Returns:
+        Object that represents newly created issue.
+    """
+    raise NotImplementedError()
+
+
+
+def get(project: Any, id: int) ‑> Issue +
+
+

Get issue.

+

Args

+
+
project : GitProject
+
Project where the issue is to be opened.
+
issue_id
+
ID of the issue.
+
+

Returns

+

Object that represents requested issue.

+
+ +Expand source code + +
@staticmethod
+def get(project: Any, id: int) -> "Issue":
+    """
+    Get issue.
+
+    Args:
+        project (GitProject): Project where the issue is to be opened.
+        issue_id: ID of the issue.
+
+    Returns:
+        Object that represents requested issue.
+    """
+    raise NotImplementedError()
+
+
+
+def get_list(project: Any, status: IssueStatus = IssueStatus.open, author: Optional[str] = None, assignee: Optional[str] = None, labels: Optional[list[str]] = None) ‑> list['Issue'] +
+
+

List of issues.

+

Args

+
+
project : GitProject
+
Project where the issue is to be opened.
+
status
+
+

Status of the issues that are to be +included in the list.

+

Defaults to IssueStatus.open.

+
+
author
+
+

Username of the author of the issues.

+

Defaults to no filtering by author.

+
+
assignee
+
+

Username of the assignee on the issues.

+

Defaults to no filtering by assignees.

+
+
labels
+
+

Filter issues that have set specific labels.

+

Defaults to no filtering by labels.

+
+
+

Returns

+

List of objects that represent requested issues.

+
+ +Expand source code + +
@staticmethod
+def get_list(
+    project: Any,
+    status: IssueStatus = IssueStatus.open,
+    author: Optional[str] = None,
+    assignee: Optional[str] = None,
+    labels: Optional[list[str]] = None,
+) -> list["Issue"]:
+    """
+    List of issues.
+
+    Args:
+        project (GitProject): Project where the issue is to be opened.
+        status: Status of the issues that are to be
+            included in the list.
+
+            Defaults to `IssueStatus.open`.
+        author: Username of the author of the issues.
+
+            Defaults to no filtering by author.
+        assignee: Username of the assignee on the issues.
+
+            Defaults to no filtering by assignees.
+        labels: Filter issues that have set specific labels.
+
+            Defaults to no filtering by labels.
+
+    Returns:
+        List of objects that represent requested issues.
+    """
+    raise NotImplementedError()
+
+
+
+

Instance variables

+
+
var author : str
+
+

Username of the author of the issue.

+
+ +Expand source code + +
@property
+def author(self) -> str:
+    """Username of the author of the issue."""
+    raise NotImplementedError()
+
+
+
var created : datetime.datetime
+
+

Datetime of the creation of the issue.

+
+ +Expand source code + +
@property
+def created(self) -> datetime.datetime:
+    """Datetime of the creation of the issue."""
+    raise NotImplementedError()
+
+
+
var description : str
+
+

Description of the issue.

+
+ +Expand source code + +
@property
+def description(self) -> str:
+    """Description of the issue."""
+    raise NotImplementedError()
+
+
+
var id : int
+
+

ID of the issue.

+
+ +Expand source code + +
@property
+def id(self) -> int:
+    """ID of the issue."""
+    raise NotImplementedError()
+
+
+
var labels : list
+
+

Labels of the issue.

+
+ +Expand source code + +
@property
+def labels(self) -> list:
+    """Labels of the issue."""
+    raise NotImplementedError()
+
+
+
var private : bool
+
+

True if issue is confidential, False otherwise.

+
+ +Expand source code + +
@property
+def private(self) -> bool:
+    """`True` if issue is confidential, `False` otherwise."""
+    raise NotImplementedError()
+
+
+
var statusIssueStatus
+
+

Status of the issue.

+
+ +Expand source code + +
@property
+def status(self) -> IssueStatus:
+    """Status of the issue."""
+    raise NotImplementedError()
+
+
+
var title : str
+
+

Title of the issue.

+
+ +Expand source code + +
@property
+def title(self) -> str:
+    """Title of the issue."""
+    raise NotImplementedError()
+
+
+
var url : str
+
+

Web URL of the issue.

+
+ +Expand source code + +
@property
+def url(self) -> str:
+    """Web URL of the issue."""
+    raise NotImplementedError()
+
+
+
+

Methods

+
+
+def add_assignee(self, *assignees: str) ‑> None +
+
+

Assign users to an issue.

+

Args

+
+
*assignees
+
List of logins of the assignees.
+
+
+ +Expand source code + +
def add_assignee(self, *assignees: str) -> None:
+    """
+    Assign users to an issue.
+
+    Args:
+        *assignees: List of logins of the assignees.
+    """
+    raise NotImplementedError()
+
+
+
+def add_label(self, *labels: str) ‑> None +
+
+

Add labels to the issue.

+

Args

+
+
*labels
+
Labels to be added.
+
+
+ +Expand source code + +
def add_label(self, *labels: str) -> None:
+    """
+    Add labels to the issue.
+
+    Args:
+        *labels: Labels to be added.
+    """
+    raise NotImplementedError()
+
+
+
+def can_close(self, username: str) ‑> bool +
+
+

Check if user have permissions to modify an issue.

+

Args

+
+
username
+
Login of the user.
+
+

Returns

+

True if user can close the issue, False otherwise.

+
+ +Expand source code + +
def can_close(self, username: str) -> bool:
+    """
+    Check if user have permissions to modify an issue.
+
+    Args:
+        username: Login of the user.
+
+    Returns:
+        `True` if user can close the issue, `False` otherwise.
+    """
+    raise NotImplementedError()
+
+
+
+def close(self) ‑> Issue +
+
+

Close an issue.

+

Returns

+

Issue itself.

+
+ +Expand source code + +
def close(self) -> "Issue":
+    """
+    Close an issue.
+
+    Returns:
+        Issue itself.
+    """
+    raise NotImplementedError()
+
+
+
+def comment(self, body: str) ‑> IssueComment +
+
+

Add new comment to the issue.

+

Args

+
+
body
+
Text contents of the comment.
+
+

Returns

+

Object that represents posted comment.

+
+ +Expand source code + +
def comment(self, body: str) -> IssueComment:
+    """
+    Add new comment to the issue.
+
+    Args:
+        body: Text contents of the comment.
+
+    Returns:
+        Object that represents posted comment.
+    """
+    raise NotImplementedError()
+
+
+
+def get_comment(self, comment_id: int) ‑> IssueComment +
+
+

Returns an issue comment.

+

Args

+
+
comment_id
+
id of a comment
+
+

Returns

+

Object representing an issue comment.

+
+ +Expand source code + +
def get_comment(self, comment_id: int) -> IssueComment:
+    """
+    Returns an issue comment.
+
+    Args:
+        comment_id: id of a comment
+
+    Returns:
+        Object representing an issue comment.
+    """
+    raise NotImplementedError()
+
+
+
+def get_comments(self, filter_regex: Optional[str] = None, reverse: bool = False, author: Optional[str] = None) ‑> list[IssueComment] +
+
+

Get list of issue comments.

+

Args

+
+
filter_regex
+
+

Filter the comments' content with re.search.

+

Defaults to None, which means no filtering.

+
+
reverse
+
+

Whether the comments are to be returned in +reversed order.

+

Defaults to False.

+
+
author
+
+

Filter the comments by author.

+

Defaults to None, which means no filtering.

+
+
+

Returns

+

List of issue comments.

+
+ +Expand source code + +
def get_comments(
+    self,
+    filter_regex: Optional[str] = None,
+    reverse: bool = False,
+    author: Optional[str] = None,
+) -> list[IssueComment]:
+    """
+    Get list of issue comments.
+
+    Args:
+        filter_regex: Filter the comments' content with `re.search`.
+
+            Defaults to `None`, which means no filtering.
+        reverse: Whether the comments are to be returned in
+            reversed order.
+
+            Defaults to `False`.
+        author: Filter the comments by author.
+
+            Defaults to `None`, which means no filtering.
+
+    Returns:
+        List of issue comments.
+    """
+    raise NotImplementedError()
+
+
+
+
+
+class IssueComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class IssueComment(Comment):
+    @property
+    def issue(self) -> "Issue":
+        """Issue of issue comment."""
+        return self._parent
+
+    def __str__(self) -> str:
+        return "Issue" + super().__str__()
+
+

Ancestors

+ +

Subclasses

+ +

Instance variables

+
+
var issueIssue
+
+

Issue of issue comment.

+
+ +Expand source code + +
@property
+def issue(self) -> "Issue":
+    """Issue of issue comment."""
+    return self._parent
+
+
+
+

Inherited members

+ +
+
+class IssueStatus +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Enumeration for issue statuses.

+
+ +Expand source code + +
class IssueStatus(IntEnum):
+    """Enumeration for issue statuses."""
+
+    open = 1
+    closed = 2
+    all = 3
+
+

Ancestors

+
    +
  • enum.IntEnum
  • +
  • builtins.int
  • +
  • enum.Enum
  • +
+

Class variables

+
+
var all
+
+
+
+
var closed
+
+
+
+
var open
+
+
+
+
+
+
+class MergeCommitStatus +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Enumeration that represents possible states of merge states of PR/MR.

+
+ +Expand source code + +
class MergeCommitStatus(Enum):
+    """Enumeration that represents possible states of merge states of PR/MR."""
+
+    can_be_merged = 1
+    cannot_be_merged = 2
+    unchecked = 3
+    checking = 4
+    cannot_be_merged_recheck = 5
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var can_be_merged
+
+
+
+
var cannot_be_merged
+
+
+
+
var cannot_be_merged_recheck
+
+
+
+
var checking
+
+
+
+
var unchecked
+
+
+
+
+
+
+class OgrAbstractClass +
+
+
+
+ +Expand source code + +
class OgrAbstractClass(metaclass=CatchCommonErrors):
+    def __repr__(self) -> str:
+        return f"<{self!s}>"
+
+

Subclasses

+ +
+
+class PRComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class PRComment(Comment):
+    @property
+    def pull_request(self) -> "PullRequest":
+        """Pull request of pull request comment."""
+        return self._parent
+
+    def __str__(self) -> str:
+        return "PR" + super().__str__()
+
+

Ancestors

+ +

Subclasses

+ +

Instance variables

+
+
var pull_requestPullRequest
+
+

Pull request of pull request comment.

+
+ +Expand source code + +
@property
+def pull_request(self) -> "PullRequest":
+    """Pull request of pull request comment."""
+    return self._parent
+
+
+
+

Inherited members

+ +
+
+class PRStatus +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Enumeration that represents statuses of pull requests.

+
+ +Expand source code + +
class PRStatus(IntEnum):
+    """Enumeration that represents statuses of pull requests."""
+
+    open = 1
+    closed = 2
+    merged = 3
+    all = 4
+
+

Ancestors

+
    +
  • enum.IntEnum
  • +
  • builtins.int
  • +
  • enum.Enum
  • +
+

Class variables

+
+
var all
+
+
+
+
var closed
+
+
+
+
var merged
+
+
+
+
var open
+
+
+
+
+
+
+class PullRequest +(raw_pr: Any, project: GitProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the pull request.
+
+
+ +Expand source code + +
class PullRequest(OgrAbstractClass):
+    """
+    Attributes:
+        project (GitProject): Project of the pull request.
+    """
+
+    def __init__(self, raw_pr: Any, project: "GitProject") -> None:
+        self._raw_pr = raw_pr
+        self._target_project = project
+
+    @property
+    def title(self) -> str:
+        """Title of the pull request."""
+        raise NotImplementedError()
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        raise NotImplementedError()
+
+    @property
+    def id(self) -> int:
+        """ID of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def status(self) -> PRStatus:
+        """Status of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def url(self) -> str:
+        """Web URL of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def description(self) -> str:
+        """Description of the pull request."""
+        raise NotImplementedError()
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        raise NotImplementedError
+
+    @property
+    def author(self) -> str:
+        """Login of the author of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def source_branch(self) -> str:
+        """Name of the source branch (from which the changes are pulled)."""
+        raise NotImplementedError()
+
+    @property
+    def target_branch(self) -> str:
+        """Name of the target branch (where the changes are being merged)."""
+        raise NotImplementedError()
+
+    @property
+    def created(self) -> datetime.datetime:
+        """Datetime of creating the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def labels(self) -> list[Any]:
+        """Labels of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def diff_url(self) -> str:
+        """Web URL to the diff of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def patch(self) -> bytes:
+        """Patch of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def head_commit(self) -> str:
+        """Commit hash of the HEAD commit of the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def target_branch_head_commit(self) -> str:
+        """Commit hash of the HEAD commit of the target branch."""
+        raise NotImplementedError()
+
+    @property
+    def merge_commit_sha(self) -> str:
+        """
+        Commit hash of the merge commit of the pull request.
+
+        Before merging represents test merge commit, if git forge supports it.
+        """
+        raise NotImplementedError()
+
+    @property
+    def merge_commit_status(self) -> MergeCommitStatus:
+        """Current status of the test merge commit."""
+        raise NotImplementedError()
+
+    @property
+    def source_project(self) -> "GitProject":
+        """Object that represents source project (from which the changes are pulled)."""
+        raise NotImplementedError()
+
+    @property
+    def target_project(self) -> "GitProject":
+        """Object that represents target project (where changes are merged)."""
+        return self._target_project
+
+    @property
+    def commits_url(self) -> str:
+        """Web URL to the list of commits in the pull request."""
+        raise NotImplementedError()
+
+    @property
+    def closed_by(self) -> Optional[str]:
+        """Login of the account that closed the pull request."""
+        raise NotImplementedError
+
+    def __str__(self) -> str:
+        description = (
+            f"{self.description[:10]}..." if self.description is not None else "None"
+        )
+        return (
+            f"PullRequest("
+            f"title='{self.title}', "
+            f"id={self.id}, "
+            f"status='{self.status.name}', "
+            f"url='{self.url}', "
+            f"diff_url='{self.diff_url}', "
+            f"description='{description}', "
+            f"author='{self.author}', "
+            f"source_branch='{self.source_branch}', "
+            f"target_branch='{self.target_branch}', "
+            f"created='{self.created}')"
+        )
+
+    @staticmethod
+    def create(
+        project: Any,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        Create new pull request.
+
+        Args:
+            project (GitProject): Project where the pull request will be created.
+            title: Title of the pull request.
+            body: Description of the pull request.
+            target_branch: Branch in the project where the changes are being
+                merged.
+            source_branch: Branch from which the changes are being pulled.
+            fork_username: The username/namespace of the forked repository.
+
+        Returns:
+            Object that represents newly created pull request.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get(project: Any, id: int) -> "PullRequest":
+        """
+        Get pull request.
+
+        Args:
+            project (GitProject): Project where the pull request is located.
+            id: ID of the pull request.
+
+        Returns:
+            Object that represents pull request.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get_list(project: Any, status: PRStatus = PRStatus.open) -> list["PullRequest"]:
+        """
+        List of pull requests.
+
+        Args:
+            project (GitProject): Project where the pull requests are located.
+            status: Filters out the pull requests.
+
+                Defaults to `PRStatus.open`.
+
+        Returns:
+            List of pull requests with requested status.
+        """
+        raise NotImplementedError()
+
+    def update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        Update pull request information.
+
+        Args:
+            title: The new title of the pull request.
+
+                Defaults to `None`, which means no updating.
+            description: The new description of the pull request.
+
+                Defaults to `None`, which means no updating.
+
+        Returns:
+            Pull request itself.
+        """
+        raise NotImplementedError()
+
+    def _get_all_comments(self) -> list[PRComment]:
+        """
+        Get list of all pull request comments.
+
+        Returns:
+            List of all comments on the pull request.
+        """
+        raise NotImplementedError()
+
+    def get_comments(
+        self,
+        filter_regex: Optional[str] = None,
+        reverse: bool = False,
+        author: Optional[str] = None,
+    ) -> list["PRComment"]:
+        """
+        Get list of pull request comments.
+
+        Args:
+            filter_regex: Filter the comments' content with `re.search`.
+
+                Defaults to `None`, which means no filtering.
+            reverse: Whether the comments are to be returned in
+                reversed order.
+
+                Defaults to `False`.
+            author: Filter the comments by author.
+
+                Defaults to `None`, which means no filtering.
+
+        Returns:
+            List of pull request comments.
+        """
+        raise NotImplementedError()
+
+    def get_all_commits(self) -> list[str]:
+        """
+        Returns:
+            List of commit hashes of commits in pull request.
+        """
+        raise NotImplementedError()
+
+    def search(
+        self,
+        filter_regex: str,
+        reverse: bool = False,
+        description: bool = True,
+    ) -> Optional[Match[str]]:
+        """
+        Find match in pull request description or comments.
+
+        Args:
+            filter_regex: Regex that is used to filter the comments' content with `re.search`.
+            reverse: Reverse order of the comments.
+
+                Defaults to `False`.
+            description: Whether description is included in the search.
+
+                Defaults to `True`.
+
+        Returns:
+            `re.Match` if found, `None` otherwise.
+        """
+        raise NotImplementedError()
+
+    def comment(
+        self,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        """
+        Add new comment to the pull request.
+
+        Args:
+            body: Body of the comment.
+            commit: Commit hash to which comment is related.
+
+                Defaults to generic comment.
+            filename: Path to the file to which comment is related.
+
+                Defaults to no relation to the file.
+            row: Line number to which the comment is related.
+
+                Defaults to no relation to the line.
+
+        Returns:
+            Newly created comment.
+        """
+        raise NotImplementedError()
+
+    def close(self) -> "PullRequest":
+        """
+        Close the pull request.
+
+        Returns:
+            Pull request itself.
+        """
+        raise NotImplementedError()
+
+    def merge(self) -> "PullRequest":
+        """
+        Merge the pull request.
+
+        Returns:
+            Pull request itself.
+        """
+        raise NotImplementedError()
+
+    def add_label(self, *labels: str) -> None:
+        """
+        Add labels to the pull request.
+
+        Args:
+            *labels: Labels to be added.
+        """
+        raise NotImplementedError()
+
+    def get_statuses(self) -> list["CommitFlag"]:
+        """
+        Returns statuses for latest commit on pull request.
+
+        Returns:
+            List of commit statuses of the latest commit.
+        """
+        raise NotImplementedError()
+
+    def get_comment(self, comment_id: int) -> PRComment:
+        """
+        Returns a PR comment.
+
+        Args:
+            comment_id: id of comment
+
+        Returns:
+            Object representing a PR comment.
+        """
+        raise NotImplementedError()
+
+

Ancestors

+ +

Subclasses

+ +

Static methods

+
+
+def create(project: Any, title: str, body: str, target_branch: str, source_branch: str, fork_username: Optional[str] = None) ‑> PullRequest +
+
+

Create new pull request.

+

Args

+
+
project : GitProject
+
Project where the pull request will be created.
+
title
+
Title of the pull request.
+
body
+
Description of the pull request.
+
target_branch
+
Branch in the project where the changes are being +merged.
+
source_branch
+
Branch from which the changes are being pulled.
+
fork_username
+
The username/namespace of the forked repository.
+
+

Returns

+

Object that represents newly created pull request.

+
+ +Expand source code + +
@staticmethod
+def create(
+    project: Any,
+    title: str,
+    body: str,
+    target_branch: str,
+    source_branch: str,
+    fork_username: Optional[str] = None,
+) -> "PullRequest":
+    """
+    Create new pull request.
+
+    Args:
+        project (GitProject): Project where the pull request will be created.
+        title: Title of the pull request.
+        body: Description of the pull request.
+        target_branch: Branch in the project where the changes are being
+            merged.
+        source_branch: Branch from which the changes are being pulled.
+        fork_username: The username/namespace of the forked repository.
+
+    Returns:
+        Object that represents newly created pull request.
+    """
+    raise NotImplementedError()
+
+
+
+def get(project: Any, id: int) ‑> PullRequest +
+
+

Get pull request.

+

Args

+
+
project : GitProject
+
Project where the pull request is located.
+
id
+
ID of the pull request.
+
+

Returns

+

Object that represents pull request.

+
+ +Expand source code + +
@staticmethod
+def get(project: Any, id: int) -> "PullRequest":
+    """
+    Get pull request.
+
+    Args:
+        project (GitProject): Project where the pull request is located.
+        id: ID of the pull request.
+
+    Returns:
+        Object that represents pull request.
+    """
+    raise NotImplementedError()
+
+
+
+def get_list(project: Any, status: PRStatus = PRStatus.open) ‑> list['PullRequest'] +
+
+

List of pull requests.

+

Args

+
+
project : GitProject
+
Project where the pull requests are located.
+
status
+
+

Filters out the pull requests.

+

Defaults to PRStatus.open.

+
+
+

Returns

+

List of pull requests with requested status.

+
+ +Expand source code + +
@staticmethod
+def get_list(project: Any, status: PRStatus = PRStatus.open) -> list["PullRequest"]:
+    """
+    List of pull requests.
+
+    Args:
+        project (GitProject): Project where the pull requests are located.
+        status: Filters out the pull requests.
+
+            Defaults to `PRStatus.open`.
+
+    Returns:
+        List of pull requests with requested status.
+    """
+    raise NotImplementedError()
+
+
+
+

Instance variables

+
+
var author : str
+
+

Login of the author of the pull request.

+
+ +Expand source code + +
@property
+def author(self) -> str:
+    """Login of the author of the pull request."""
+    raise NotImplementedError()
+
+
+
var closed_by : Optional[str]
+
+

Login of the account that closed the pull request.

+
+ +Expand source code + +
@property
+def closed_by(self) -> Optional[str]:
+    """Login of the account that closed the pull request."""
+    raise NotImplementedError
+
+
+
var commits_url : str
+
+

Web URL to the list of commits in the pull request.

+
+ +Expand source code + +
@property
+def commits_url(self) -> str:
+    """Web URL to the list of commits in the pull request."""
+    raise NotImplementedError()
+
+
+
var created : datetime.datetime
+
+

Datetime of creating the pull request.

+
+ +Expand source code + +
@property
+def created(self) -> datetime.datetime:
+    """Datetime of creating the pull request."""
+    raise NotImplementedError()
+
+
+
var description : str
+
+

Description of the pull request.

+
+ +Expand source code + +
@property
+def description(self) -> str:
+    """Description of the pull request."""
+    raise NotImplementedError()
+
+
+
var diff_url : str
+
+

Web URL to the diff of the pull request.

+
+ +Expand source code + +
@property
+def diff_url(self) -> str:
+    """Web URL to the diff of the pull request."""
+    raise NotImplementedError()
+
+
+
var head_commit : str
+
+

Commit hash of the HEAD commit of the pull request.

+
+ +Expand source code + +
@property
+def head_commit(self) -> str:
+    """Commit hash of the HEAD commit of the pull request."""
+    raise NotImplementedError()
+
+
+
var id : int
+
+

ID of the pull request.

+
+ +Expand source code + +
@property
+def id(self) -> int:
+    """ID of the pull request."""
+    raise NotImplementedError()
+
+
+
var labels : list[typing.Any]
+
+

Labels of the pull request.

+
+ +Expand source code + +
@property
+def labels(self) -> list[Any]:
+    """Labels of the pull request."""
+    raise NotImplementedError()
+
+
+
var merge_commit_sha : str
+
+

Commit hash of the merge commit of the pull request.

+

Before merging represents test merge commit, if git forge supports it.

+
+ +Expand source code + +
@property
+def merge_commit_sha(self) -> str:
+    """
+    Commit hash of the merge commit of the pull request.
+
+    Before merging represents test merge commit, if git forge supports it.
+    """
+    raise NotImplementedError()
+
+
+
var merge_commit_statusMergeCommitStatus
+
+

Current status of the test merge commit.

+
+ +Expand source code + +
@property
+def merge_commit_status(self) -> MergeCommitStatus:
+    """Current status of the test merge commit."""
+    raise NotImplementedError()
+
+
+
var patch : bytes
+
+

Patch of the pull request.

+
+ +Expand source code + +
@property
+def patch(self) -> bytes:
+    """Patch of the pull request."""
+    raise NotImplementedError()
+
+
+
var source_branch : str
+
+

Name of the source branch (from which the changes are pulled).

+
+ +Expand source code + +
@property
+def source_branch(self) -> str:
+    """Name of the source branch (from which the changes are pulled)."""
+    raise NotImplementedError()
+
+
+
var source_projectGitProject
+
+

Object that represents source project (from which the changes are pulled).

+
+ +Expand source code + +
@property
+def source_project(self) -> "GitProject":
+    """Object that represents source project (from which the changes are pulled)."""
+    raise NotImplementedError()
+
+
+
var statusPRStatus
+
+

Status of the pull request.

+
+ +Expand source code + +
@property
+def status(self) -> PRStatus:
+    """Status of the pull request."""
+    raise NotImplementedError()
+
+
+
var target_branch : str
+
+

Name of the target branch (where the changes are being merged).

+
+ +Expand source code + +
@property
+def target_branch(self) -> str:
+    """Name of the target branch (where the changes are being merged)."""
+    raise NotImplementedError()
+
+
+
var target_branch_head_commit : str
+
+

Commit hash of the HEAD commit of the target branch.

+
+ +Expand source code + +
@property
+def target_branch_head_commit(self) -> str:
+    """Commit hash of the HEAD commit of the target branch."""
+    raise NotImplementedError()
+
+
+
var target_projectGitProject
+
+

Object that represents target project (where changes are merged).

+
+ +Expand source code + +
@property
+def target_project(self) -> "GitProject":
+    """Object that represents target project (where changes are merged)."""
+    return self._target_project
+
+
+
var title : str
+
+

Title of the pull request.

+
+ +Expand source code + +
@property
+def title(self) -> str:
+    """Title of the pull request."""
+    raise NotImplementedError()
+
+
+
var url : str
+
+

Web URL of the pull request.

+
+ +Expand source code + +
@property
+def url(self) -> str:
+    """Web URL of the pull request."""
+    raise NotImplementedError()
+
+
+
+

Methods

+
+
+def add_label(self, *labels: str) ‑> None +
+
+

Add labels to the pull request.

+

Args

+
+
*labels
+
Labels to be added.
+
+
+ +Expand source code + +
def add_label(self, *labels: str) -> None:
+    """
+    Add labels to the pull request.
+
+    Args:
+        *labels: Labels to be added.
+    """
+    raise NotImplementedError()
+
+
+
+def close(self) ‑> PullRequest +
+
+

Close the pull request.

+

Returns

+

Pull request itself.

+
+ +Expand source code + +
def close(self) -> "PullRequest":
+    """
+    Close the pull request.
+
+    Returns:
+        Pull request itself.
+    """
+    raise NotImplementedError()
+
+
+
+def comment(self, body: str, commit: Optional[str] = None, filename: Optional[str] = None, row: Optional[int] = None) ‑> PRComment +
+
+

Add new comment to the pull request.

+

Args

+
+
body
+
Body of the comment.
+
commit
+
+

Commit hash to which comment is related.

+

Defaults to generic comment.

+
+
filename
+
+

Path to the file to which comment is related.

+

Defaults to no relation to the file.

+
+
row
+
+

Line number to which the comment is related.

+

Defaults to no relation to the line.

+
+
+

Returns

+

Newly created comment.

+
+ +Expand source code + +
def comment(
+    self,
+    body: str,
+    commit: Optional[str] = None,
+    filename: Optional[str] = None,
+    row: Optional[int] = None,
+) -> "PRComment":
+    """
+    Add new comment to the pull request.
+
+    Args:
+        body: Body of the comment.
+        commit: Commit hash to which comment is related.
+
+            Defaults to generic comment.
+        filename: Path to the file to which comment is related.
+
+            Defaults to no relation to the file.
+        row: Line number to which the comment is related.
+
+            Defaults to no relation to the line.
+
+    Returns:
+        Newly created comment.
+    """
+    raise NotImplementedError()
+
+
+
+def get_all_commits(self) ‑> list[str] +
+
+

Returns

+

List of commit hashes of commits in pull request.

+
+ +Expand source code + +
def get_all_commits(self) -> list[str]:
+    """
+    Returns:
+        List of commit hashes of commits in pull request.
+    """
+    raise NotImplementedError()
+
+
+
+def get_comment(self, comment_id: int) ‑> PRComment +
+
+

Returns a PR comment.

+

Args

+
+
comment_id
+
id of comment
+
+

Returns

+

Object representing a PR comment.

+
+ +Expand source code + +
def get_comment(self, comment_id: int) -> PRComment:
+    """
+    Returns a PR comment.
+
+    Args:
+        comment_id: id of comment
+
+    Returns:
+        Object representing a PR comment.
+    """
+    raise NotImplementedError()
+
+
+
+def get_comments(self, filter_regex: Optional[str] = None, reverse: bool = False, author: Optional[str] = None) ‑> list['PRComment'] +
+
+

Get list of pull request comments.

+

Args

+
+
filter_regex
+
+

Filter the comments' content with re.search.

+

Defaults to None, which means no filtering.

+
+
reverse
+
+

Whether the comments are to be returned in +reversed order.

+

Defaults to False.

+
+
author
+
+

Filter the comments by author.

+

Defaults to None, which means no filtering.

+
+
+

Returns

+

List of pull request comments.

+
+ +Expand source code + +
def get_comments(
+    self,
+    filter_regex: Optional[str] = None,
+    reverse: bool = False,
+    author: Optional[str] = None,
+) -> list["PRComment"]:
+    """
+    Get list of pull request comments.
+
+    Args:
+        filter_regex: Filter the comments' content with `re.search`.
+
+            Defaults to `None`, which means no filtering.
+        reverse: Whether the comments are to be returned in
+            reversed order.
+
+            Defaults to `False`.
+        author: Filter the comments by author.
+
+            Defaults to `None`, which means no filtering.
+
+    Returns:
+        List of pull request comments.
+    """
+    raise NotImplementedError()
+
+
+
+def get_statuses(self) ‑> list['CommitFlag'] +
+
+

Returns statuses for latest commit on pull request.

+

Returns

+

List of commit statuses of the latest commit.

+
+ +Expand source code + +
def get_statuses(self) -> list["CommitFlag"]:
+    """
+    Returns statuses for latest commit on pull request.
+
+    Returns:
+        List of commit statuses of the latest commit.
+    """
+    raise NotImplementedError()
+
+
+
+def merge(self) ‑> PullRequest +
+
+

Merge the pull request.

+

Returns

+

Pull request itself.

+
+ +Expand source code + +
def merge(self) -> "PullRequest":
+    """
+    Merge the pull request.
+
+    Returns:
+        Pull request itself.
+    """
+    raise NotImplementedError()
+
+
+
+def search(self, filter_regex: str, reverse: bool = False, description: bool = True) ‑> Optional[re.Match[str]] +
+
+

Find match in pull request description or comments.

+

Args

+
+
filter_regex
+
Regex that is used to filter the comments' content with re.search.
+
reverse
+
+

Reverse order of the comments.

+

Defaults to False.

+
+
description
+
+

Whether description is included in the search.

+

Defaults to True.

+
+
+

Returns

+

re.Match if found, None otherwise.

+
+ +Expand source code + +
def search(
+    self,
+    filter_regex: str,
+    reverse: bool = False,
+    description: bool = True,
+) -> Optional[Match[str]]:
+    """
+    Find match in pull request description or comments.
+
+    Args:
+        filter_regex: Regex that is used to filter the comments' content with `re.search`.
+        reverse: Reverse order of the comments.
+
+            Defaults to `False`.
+        description: Whether description is included in the search.
+
+            Defaults to `True`.
+
+    Returns:
+        `re.Match` if found, `None` otherwise.
+    """
+    raise NotImplementedError()
+
+
+
+def update_info(self, title: Optional[str] = None, description: Optional[str] = None) ‑> PullRequest +
+
+

Update pull request information.

+

Args

+
+
title
+
+

The new title of the pull request.

+

Defaults to None, which means no updating.

+
+
description
+
+

The new description of the pull request.

+

Defaults to None, which means no updating.

+
+
+

Returns

+

Pull request itself.

+
+ +Expand source code + +
def update_info(
+    self,
+    title: Optional[str] = None,
+    description: Optional[str] = None,
+) -> "PullRequest":
+    """
+    Update pull request information.
+
+    Args:
+        title: The new title of the pull request.
+
+            Defaults to `None`, which means no updating.
+        description: The new description of the pull request.
+
+            Defaults to `None`, which means no updating.
+
+    Returns:
+        Pull request itself.
+    """
+    raise NotImplementedError()
+
+
+
+
+
+class Reaction +(raw_reaction: Any) +
+
+
+
+ +Expand source code + +
class Reaction(OgrAbstractClass):
+    def __init__(self, raw_reaction: Any) -> None:
+        self._raw_reaction = raw_reaction
+
+    def __str__(self):
+        return f"Reaction(raw_reaction={self._raw_reaction})"
+
+    def delete(self) -> None:
+        """Delete a reaction."""
+        raise NotImplementedError()
+
+

Ancestors

+ +

Subclasses

+ +

Methods

+
+
+def delete(self) ‑> None +
+
+

Delete a reaction.

+
+ +Expand source code + +
def delete(self) -> None:
+    """Delete a reaction."""
+    raise NotImplementedError()
+
+
+
+
+
+class Release +(raw_release: Any, project: GitProject) +
+
+

Object that represents release.

+

Attributes

+
+
project : GitProject
+
Project on which the release is created.
+
+
+ +Expand source code + +
class Release(OgrAbstractClass):
+    """
+    Object that represents release.
+
+    Attributes:
+        project (GitProject): Project on which the release is created.
+    """
+
+    def __init__(
+        self,
+        raw_release: Any,
+        project: "GitProject",
+    ) -> None:
+        self._raw_release = raw_release
+        self.project = project
+
+    def __str__(self) -> str:
+        return (
+            f"Release("
+            f"title='{self.title}', "
+            f"body='{self.body}', "
+            f"tag_name='{self.tag_name}', "
+            f"url='{self.url}', "
+            f"created_at='{self.created_at}', "
+            f"tarball_url='{self.tarball_url}')"
+        )
+
+    @property
+    def title(self) -> str:
+        """Title of the release."""
+        raise NotImplementedError()
+
+    @property
+    def body(self) -> str:
+        """Body of the release."""
+        raise NotImplementedError()
+
+    @property
+    def git_tag(self) -> GitTag:
+        """Object that represents tag tied to the release."""
+        raise NotImplementedError()
+
+    @property
+    def tag_name(self) -> str:
+        """Tag tied to the release."""
+        raise NotImplementedError()
+
+    @property
+    def url(self) -> Optional[str]:
+        """URL of the release."""
+        raise NotImplementedError()
+
+    # TODO: Check if should really be string
+    @property
+    def created_at(self) -> datetime.datetime:
+        """Datetime of creating the release."""
+        raise NotImplementedError()
+
+    @property
+    def tarball_url(self) -> str:
+        """URL of the tarball."""
+        raise NotImplementedError()
+
+    @staticmethod
+    def get(
+        project: Any,
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> "Release":
+        """
+        Get a single release.
+
+        Args:
+            identifier: Identifier of the release.
+
+                Defaults to `None`, which means not being used.
+            name: Name of the release.
+
+                Defaults to `None`, which means not being used.
+            tag_name: Tag that the release is tied to.
+
+                Defaults to `None`, which means not being used.
+
+        Returns:
+            Object that represents release that satisfies requested condition.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get_latest(project: Any) -> Optional["Release"]:
+        """
+        Returns:
+            Object that represents the latest release.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def get_list(project: Any) -> list["Release"]:
+        """
+        Returns:
+            List of the objects that represent releases.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def create(
+        project: Any,
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> "Release":
+        """
+        Create new release.
+
+        Args:
+            project: Project where the release is to be created.
+            tag: Tag which is the release based off.
+            name: Name of the release.
+            message: Message or description of the release.
+            ref: Git reference, mainly commit hash for the release. If provided
+                git tag is created prior to creating a release.
+
+                Defaults to `None`.
+
+        Returns:
+            Object that represents newly created release.
+        """
+        raise NotImplementedError()
+
+    def save_archive(self, filename: str) -> None:
+        """
+        Save tarball of the release to requested `filename`.
+
+        Args:
+            filename: Path to the file to save archive to.
+        """
+        raise NotImplementedError()
+
+    def edit_release(self, name: str, message: str) -> None:
+        """
+        Edit name and message of a release.
+
+        Args:
+            name: Name of the release.
+            message: Description of the release.
+        """
+        raise NotImplementedError()
+
+

Ancestors

+ +

Subclasses

+ +

Static methods

+
+
+def create(project: Any, tag: str, name: str, message: str, ref: Optional[str] = None) ‑> Release +
+
+

Create new release.

+

Args

+
+
project
+
Project where the release is to be created.
+
tag
+
Tag which is the release based off.
+
name
+
Name of the release.
+
message
+
Message or description of the release.
+
ref
+
+

Git reference, mainly commit hash for the release. If provided +git tag is created prior to creating a release.

+

Defaults to None.

+
+
+

Returns

+

Object that represents newly created release.

+
+ +Expand source code + +
@staticmethod
+def create(
+    project: Any,
+    tag: str,
+    name: str,
+    message: str,
+    ref: Optional[str] = None,
+) -> "Release":
+    """
+    Create new release.
+
+    Args:
+        project: Project where the release is to be created.
+        tag: Tag which is the release based off.
+        name: Name of the release.
+        message: Message or description of the release.
+        ref: Git reference, mainly commit hash for the release. If provided
+            git tag is created prior to creating a release.
+
+            Defaults to `None`.
+
+    Returns:
+        Object that represents newly created release.
+    """
+    raise NotImplementedError()
+
+
+
+def get(project: Any, identifier: Optional[int] = None, name: Optional[str] = None, tag_name: Optional[str] = None) ‑> Release +
+
+

Get a single release.

+

Args

+
+
identifier
+
+

Identifier of the release.

+

Defaults to None, which means not being used.

+
+
name
+
+

Name of the release.

+

Defaults to None, which means not being used.

+
+
tag_name
+
+

Tag that the release is tied to.

+

Defaults to None, which means not being used.

+
+
+

Returns

+

Object that represents release that satisfies requested condition.

+
+ +Expand source code + +
@staticmethod
+def get(
+    project: Any,
+    identifier: Optional[int] = None,
+    name: Optional[str] = None,
+    tag_name: Optional[str] = None,
+) -> "Release":
+    """
+    Get a single release.
+
+    Args:
+        identifier: Identifier of the release.
+
+            Defaults to `None`, which means not being used.
+        name: Name of the release.
+
+            Defaults to `None`, which means not being used.
+        tag_name: Tag that the release is tied to.
+
+            Defaults to `None`, which means not being used.
+
+    Returns:
+        Object that represents release that satisfies requested condition.
+    """
+    raise NotImplementedError()
+
+
+
+def get_latest(project: Any) ‑> Optional[Release] +
+
+

Returns

+

Object that represents the latest release.

+
+ +Expand source code + +
@staticmethod
+def get_latest(project: Any) -> Optional["Release"]:
+    """
+    Returns:
+        Object that represents the latest release.
+    """
+    raise NotImplementedError()
+
+
+
+def get_list(project: Any) ‑> list['Release'] +
+
+

Returns

+

List of the objects that represent releases.

+
+ +Expand source code + +
@staticmethod
+def get_list(project: Any) -> list["Release"]:
+    """
+    Returns:
+        List of the objects that represent releases.
+    """
+    raise NotImplementedError()
+
+
+
+

Instance variables

+
+
var body : str
+
+

Body of the release.

+
+ +Expand source code + +
@property
+def body(self) -> str:
+    """Body of the release."""
+    raise NotImplementedError()
+
+
+
var created_at : datetime.datetime
+
+

Datetime of creating the release.

+
+ +Expand source code + +
@property
+def created_at(self) -> datetime.datetime:
+    """Datetime of creating the release."""
+    raise NotImplementedError()
+
+
+
var git_tagGitTag
+
+

Object that represents tag tied to the release.

+
+ +Expand source code + +
@property
+def git_tag(self) -> GitTag:
+    """Object that represents tag tied to the release."""
+    raise NotImplementedError()
+
+
+
var tag_name : str
+
+

Tag tied to the release.

+
+ +Expand source code + +
@property
+def tag_name(self) -> str:
+    """Tag tied to the release."""
+    raise NotImplementedError()
+
+
+
var tarball_url : str
+
+

URL of the tarball.

+
+ +Expand source code + +
@property
+def tarball_url(self) -> str:
+    """URL of the tarball."""
+    raise NotImplementedError()
+
+
+
var title : str
+
+

Title of the release.

+
+ +Expand source code + +
@property
+def title(self) -> str:
+    """Title of the release."""
+    raise NotImplementedError()
+
+
+
var url : Optional[str]
+
+

URL of the release.

+
+ +Expand source code + +
@property
+def url(self) -> Optional[str]:
+    """URL of the release."""
+    raise NotImplementedError()
+
+
+
+

Methods

+
+
+def edit_release(self, name: str, message: str) ‑> None +
+
+

Edit name and message of a release.

+

Args

+
+
name
+
Name of the release.
+
message
+
Description of the release.
+
+
+ +Expand source code + +
def edit_release(self, name: str, message: str) -> None:
+    """
+    Edit name and message of a release.
+
+    Args:
+        name: Name of the release.
+        message: Description of the release.
+    """
+    raise NotImplementedError()
+
+
+
+def save_archive(self, filename: str) ‑> None +
+
+

Save tarball of the release to requested filename.

+

Args

+
+
filename
+
Path to the file to save archive to.
+
+
+ +Expand source code + +
def save_archive(self, filename: str) -> None:
+    """
+    Save tarball of the release to requested `filename`.
+
+    Args:
+        filename: Path to the file to save archive to.
+    """
+    raise NotImplementedError()
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/constant.html b/docs/constant.html new file mode 100644 index 00000000..0210c56c --- /dev/null +++ b/docs/constant.html @@ -0,0 +1,63 @@ + + + + + + +ogr.constant API documentation + + + + + + + + + + + +
+
+
+

Module ogr.constant

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+CLONE_TIMEOUT = 60
+DEFAULT_RO_PREFIX_STRING = "READ ONLY: "
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/deprecation.html b/docs/deprecation.html new file mode 100644 index 00000000..639b1412 --- /dev/null +++ b/docs/deprecation.html @@ -0,0 +1,126 @@ + + + + + + +ogr.deprecation API documentation + + + + + + + + + + + +
+
+
+

Module ogr.deprecation

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from deprecated import deprecated
+
+
+def deprecate_and_set_removal(since: str, remove_in: str, message: str):
+    """
+    Decorator for deprecating functions in ogr.
+
+    Args:
+        since: Indicates a version since which is attribute deprecated.
+        remove_in: Indicates a version in which the attribute will be removed.
+        message: Message to be included with deprecation.
+
+    Returns:
+        Decorator.
+    """
+    return deprecated(
+        version=since,
+        reason=f"will be removed in {remove_in}: {message}",
+    )
+
+
+
+
+
+
+
+

Functions

+
+
+def deprecate_and_set_removal(since: str, remove_in: str, message: str) +
+
+

Decorator for deprecating functions in ogr.

+

Args

+
+
since
+
Indicates a version since which is attribute deprecated.
+
remove_in
+
Indicates a version in which the attribute will be removed.
+
message
+
Message to be included with deprecation.
+
+

Returns

+

Decorator.

+
+ +Expand source code + +
def deprecate_and_set_removal(since: str, remove_in: str, message: str):
+    """
+    Decorator for deprecating functions in ogr.
+
+    Args:
+        since: Indicates a version since which is attribute deprecated.
+        remove_in: Indicates a version in which the attribute will be removed.
+        message: Message to be included with deprecation.
+
+    Returns:
+        Decorator.
+    """
+    return deprecated(
+        version=since,
+        reason=f"will be removed in {remove_in}: {message}",
+    )
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/exceptions.html b/docs/exceptions.html new file mode 100644 index 00000000..a111ba3f --- /dev/null +++ b/docs/exceptions.html @@ -0,0 +1,537 @@ + + + + + + +ogr.exceptions API documentation + + + + + + + + + + + +
+
+
+

Module ogr.exceptions

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from typing import Any, Optional
+
+import github
+import gitlab
+
+
+class OgrException(Exception):
+    """Something went wrong during our execution."""
+
+
+class APIException(OgrException):
+    """Generic API exception."""
+
+    @property
+    def response_code(self):
+        raise NotImplementedError()
+
+
+class PagureAPIException(APIException):
+    """Exception related to Pagure API."""
+
+    def __init__(
+        self,
+        *args: Any,
+        pagure_error: Optional[str] = None,
+        pagure_response: Optional[dict[str, Any]] = None,
+        response_code: Optional[int] = None,
+    ) -> None:
+        super().__init__(*args)
+        self._pagure_error = pagure_error
+        self.pagure_response = pagure_response
+        self._response_code = response_code
+
+    @property
+    def pagure_error(self):
+        return self._pagure_error or self.__cause__
+
+    @property
+    def response_code(self):
+        return self._response_code
+
+
+class GithubAPIException(APIException):
+    """Exception related to Github API."""
+
+    @property
+    def response_code(self):
+        if self.__cause__ is None or not isinstance(
+            self.__cause__,
+            github.GithubException,
+        ):
+            return None
+        return self.__cause__.status
+
+
+class GitlabAPIException(APIException):
+    """Exception related to Gitlab API."""
+
+    @property
+    def response_code(self):
+        if self.__cause__ is None or not isinstance(self.__cause__, gitlab.GitlabError):
+            return None
+        return self.__cause__.response_code
+
+
+class OperationNotSupported(OgrException):
+    """Raise when the operation is not supported by the backend."""
+
+
+class IssueTrackerDisabled(OperationNotSupported):
+    """Issue tracker on the project is not enabled."""
+
+
+class OgrNetworkError(OgrException):
+    """Exception raised when an unexpected network error occurs."""
+
+
+class GitForgeInternalError(OgrNetworkError):
+    """Exception raised when git forge returns internal failure."""
+
+
+class GithubAppNotInstalledError(OgrException):
+    """Exception raised when GitHub App is not installed."""
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class APIException +(*args, **kwargs) +
+
+

Generic API exception.

+
+ +Expand source code + +
class APIException(OgrException):
+    """Generic API exception."""
+
+    @property
+    def response_code(self):
+        raise NotImplementedError()
+
+

Ancestors

+
    +
  • OgrException
  • +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+

Subclasses

+ +

Instance variables

+
+
var response_code
+
+
+
+ +Expand source code + +
@property
+def response_code(self):
+    raise NotImplementedError()
+
+
+
+
+
+class GitForgeInternalError +(*args, **kwargs) +
+
+

Exception raised when git forge returns internal failure.

+
+ +Expand source code + +
class GitForgeInternalError(OgrNetworkError):
+    """Exception raised when git forge returns internal failure."""
+
+

Ancestors

+ +
+
+class GithubAPIException +(*args, **kwargs) +
+
+

Exception related to Github API.

+
+ +Expand source code + +
class GithubAPIException(APIException):
+    """Exception related to Github API."""
+
+    @property
+    def response_code(self):
+        if self.__cause__ is None or not isinstance(
+            self.__cause__,
+            github.GithubException,
+        ):
+            return None
+        return self.__cause__.status
+
+

Ancestors

+ +

Instance variables

+
+
var response_code
+
+
+
+ +Expand source code + +
@property
+def response_code(self):
+    if self.__cause__ is None or not isinstance(
+        self.__cause__,
+        github.GithubException,
+    ):
+        return None
+    return self.__cause__.status
+
+
+
+
+
+class GithubAppNotInstalledError +(*args, **kwargs) +
+
+

Exception raised when GitHub App is not installed.

+
+ +Expand source code + +
class GithubAppNotInstalledError(OgrException):
+    """Exception raised when GitHub App is not installed."""
+
+

Ancestors

+
    +
  • OgrException
  • +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+
+
+class GitlabAPIException +(*args, **kwargs) +
+
+

Exception related to Gitlab API.

+
+ +Expand source code + +
class GitlabAPIException(APIException):
+    """Exception related to Gitlab API."""
+
+    @property
+    def response_code(self):
+        if self.__cause__ is None or not isinstance(self.__cause__, gitlab.GitlabError):
+            return None
+        return self.__cause__.response_code
+
+

Ancestors

+ +

Instance variables

+
+
var response_code
+
+
+
+ +Expand source code + +
@property
+def response_code(self):
+    if self.__cause__ is None or not isinstance(self.__cause__, gitlab.GitlabError):
+        return None
+    return self.__cause__.response_code
+
+
+
+
+
+class IssueTrackerDisabled +(*args, **kwargs) +
+
+

Issue tracker on the project is not enabled.

+
+ +Expand source code + +
class IssueTrackerDisabled(OperationNotSupported):
+    """Issue tracker on the project is not enabled."""
+
+

Ancestors

+ +
+
+class OgrException +(*args, **kwargs) +
+
+

Something went wrong during our execution.

+
+ +Expand source code + +
class OgrException(Exception):
+    """Something went wrong during our execution."""
+
+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+

Subclasses

+ +
+
+class OgrNetworkError +(*args, **kwargs) +
+
+

Exception raised when an unexpected network error occurs.

+
+ +Expand source code + +
class OgrNetworkError(OgrException):
+    """Exception raised when an unexpected network error occurs."""
+
+

Ancestors

+
    +
  • OgrException
  • +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+

Subclasses

+ +
+
+class OperationNotSupported +(*args, **kwargs) +
+
+

Raise when the operation is not supported by the backend.

+
+ +Expand source code + +
class OperationNotSupported(OgrException):
+    """Raise when the operation is not supported by the backend."""
+
+

Ancestors

+
    +
  • OgrException
  • +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+

Subclasses

+ +
+
+class PagureAPIException +(*args: Any, pagure_error: Optional[str] = None, pagure_response: Optional[dict[str, typing.Any]] = None, response_code: Optional[int] = None) +
+
+

Exception related to Pagure API.

+
+ +Expand source code + +
class PagureAPIException(APIException):
+    """Exception related to Pagure API."""
+
+    def __init__(
+        self,
+        *args: Any,
+        pagure_error: Optional[str] = None,
+        pagure_response: Optional[dict[str, Any]] = None,
+        response_code: Optional[int] = None,
+    ) -> None:
+        super().__init__(*args)
+        self._pagure_error = pagure_error
+        self.pagure_response = pagure_response
+        self._response_code = response_code
+
+    @property
+    def pagure_error(self):
+        return self._pagure_error or self.__cause__
+
+    @property
+    def response_code(self):
+        return self._response_code
+
+

Ancestors

+ +

Instance variables

+
+
var pagure_error
+
+
+
+ +Expand source code + +
@property
+def pagure_error(self):
+    return self._pagure_error or self.__cause__
+
+
+
var response_code
+
+
+
+ +Expand source code + +
@property
+def response_code(self):
+    return self._response_code
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/factory.html b/docs/factory.html new file mode 100644 index 00000000..39b6c935 --- /dev/null +++ b/docs/factory.html @@ -0,0 +1,685 @@ + + + + + + +ogr.factory API documentation + + + + + + + + + + + +
+
+
+

Module ogr.factory

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import functools
+from collections.abc import Iterable
+from typing import Optional
+
+from ogr.abstract import GitProject, GitService
+from ogr.exceptions import OgrException
+from ogr.parsing import parse_git_repo
+
+_SERVICE_MAPPING: dict[str, type[GitService]] = {}
+
+
+def use_for_service(service: str, _func=None):
+    """
+    Class decorator that adds the class to the service mapping.
+
+    When the project url contains the `service` as a substring,
+    this implementation will be used to initialize the project.
+
+    When using this decorator, be sure that your class is initialized.
+    (Add the import to `ogr/__init__.py`)
+
+    Usage:
+    ```py
+    @use_for_service("github.com")
+    class GithubService(BaseGitService):
+        pass
+
+    @use_for_service("pagure.io")
+    @use_for_service("src.fedoraproject.org")
+    class PagureService(BaseGitService):
+        pass
+    ```
+
+    Args:
+        service: URL of the service.
+
+    Returns:
+        Decorator.
+    """
+
+    def decorator_cover(func):
+        @functools.wraps(func)
+        def covered_func(kls: type[GitService]):
+            _SERVICE_MAPPING[service] = kls
+            return kls
+
+        return covered_func
+
+    return decorator_cover(_func)
+
+
+def get_project(
+    url,
+    service_mapping_update: Optional[dict[str, type[GitService]]] = None,
+    custom_instances: Optional[Iterable[GitService]] = None,
+    force_custom_instance: bool = True,
+    **kwargs,
+) -> GitProject:
+    """
+    Return the project for the given URL.
+
+    Args:
+        url: URL of the project, e.g. `"https://github.com/packit/ogr"`.
+        service_mapping_update: Custom mapping from
+            service url/hostname (`str`) to service class.
+
+            Defaults to no mapping.
+        custom_instances: List of instances that will be
+            used when creating a project instance.
+
+            Defaults to `None`.
+        force_custom_instance: Force picking a Git service from the
+            `custom_instances` list, if there is any provided, raise an error if
+            that is not possible.
+
+            Defaults to `True`.
+        **kwargs: Arguments forwarded to __init__ of the matching service.
+
+    Returns:
+        `GitProject` using the matching implementation.
+    """
+    mapping = service_mapping_update.copy() if service_mapping_update else {}
+    custom_instances = custom_instances or []
+    for instance in custom_instances:
+        mapping[instance.hostname] = instance.__class__
+
+    kls = get_service_class(url=url, service_mapping_update=mapping)
+    parsed_repo_url = parse_git_repo(url)
+
+    service = None
+    if custom_instances:
+        for service_inst in custom_instances:
+            if (
+                isinstance(service_inst, kls)
+                and service_inst.hostname == parsed_repo_url.hostname
+            ):
+                service = service_inst
+                break
+        else:
+            if force_custom_instance:
+                raise OgrException(
+                    f"Instance of type {kls.__name__} "
+                    f"matching instance url '{url}' was not provided.",
+                )
+    if not service:
+        service = kls(instance_url=parsed_repo_url.get_instance_url(), **kwargs)
+    return service.get_project_from_url(url=url)
+
+
+def get_service_class_or_none(
+    url: str,
+    service_mapping_update: Optional[dict[str, type[GitService]]] = None,
+) -> Optional[type[GitService]]:
+    """
+    Get the matching service class from the URL.
+
+    Args:
+        url: URL of the project, e.g. `"https://github.com/packit/ogr"`.
+        service_mapping_update: Custom mapping from service url/hostname (`str`) to service
+            class.
+
+            Defaults to `None`.
+
+    Returns:
+        Matched class (subclass of `GitService`) or `None`.
+    """
+    mapping = {}
+    mapping.update(_SERVICE_MAPPING)
+    if service_mapping_update:
+        mapping.update(service_mapping_update)
+
+    parsed_url = parse_git_repo(url)
+    for service, service_kls in mapping.items():
+        if parse_git_repo(service).hostname in parsed_url.hostname:
+            return service_kls
+
+    return None
+
+
+def get_service_class(
+    url: str,
+    service_mapping_update: Optional[dict[str, type[GitService]]] = None,
+) -> type[GitService]:
+    """
+    Get the matching service class from the URL.
+
+    Args:
+        url: URL of the project, e.g. `"https://github.com/packit/ogr"`.
+        service_mapping_update: Custom mapping from service url/hostname (str) to service
+            class.
+
+            Defaults to `None`.
+
+    Returns:
+        Matched class (subclass of `GitService`).
+    """
+    service_kls = get_service_class_or_none(
+        url=url,
+        service_mapping_update=service_mapping_update,
+    )
+    if service_kls:
+        return service_kls
+    raise OgrException("No matching service was found.")
+
+
+def get_instances_from_dict(instances: dict) -> set[GitService]:
+    """
+    Load the service instances from the dictionary in the following form:
+
+    - `key`   : hostname, url or name that can be mapped to the service-type
+    - `value` : dictionary with arguments used when creating a new instance of the
+    service (passed to the `__init__` method)
+
+    e.g.:
+    ```py
+    get_instances_from_dict({
+        "github.com": {"token": "abcd"},
+        "pagure": {
+            "token": "abcd",
+            "instance_url": "https://src.fedoraproject.org",
+        },
+    }) == {
+        GithubService(token="abcd"),
+        PagureService(token="abcd", instance_url="https://src.fedoraproject.org")
+    }
+    ```
+
+    When the mapping `key->service-type` is not recognised, you can add a `type`
+    key to the dictionary and specify the type of the instance.
+    (It can be either name, hostname or url. The used mapping is same as for
+    key->service-type.)
+
+    The provided `key` is used as an `instance_url` and passed to the `__init__`
+    method as well.
+
+    e.g.:
+    ```py
+    get_instances_from_dict({
+        "https://my.gtlb": {"token": "abcd", "type": "gitlab"},
+    }) == {GitlabService(token="abcd", instance_url="https://my.gtlb")}
+    ```
+
+    Args:
+        instances: Mapping from service name/url/hostname to attributes for the
+            service creation.
+
+    Returns:
+        Set of the service instances.
+    """
+    services = set()
+    for key, value in instances.items():
+        service_kls = get_service_class_or_none(url=key)
+        if not service_kls:
+            if "type" not in value:
+                raise OgrException(
+                    f"No matching service was found for url '{key}'. "
+                    f"Add the service name as a `type` attribute.",
+                )
+            service_type = value["type"]
+            if service_type not in _SERVICE_MAPPING:
+                raise OgrException(
+                    f"No matching service was found for type '{service_type}'.",
+                )
+
+            service_kls = _SERVICE_MAPPING[service_type]
+            value.setdefault("instance_url", key)
+            del value["type"]
+
+        service_instance = service_kls(**value)
+        services.add(service_instance)
+
+    return services
+
+
+
+
+
+
+
+

Functions

+
+
+def get_instances_from_dict(instances: dict) ‑> set[GitService] +
+
+

Load the service instances from the dictionary in the following form:

+
    +
  • key +: hostname, url or name that can be mapped to the service-type
  • +
  • value : dictionary with arguments used when creating a new instance of the +service (passed to the __init__ method)
  • +
+

e.g.:

+
get_instances_from_dict({
+    "github.com": {"token": "abcd"},
+    "pagure": {
+        "token": "abcd",
+        "instance_url": "https://src.fedoraproject.org",
+    },
+}) == {
+    GithubService(token="abcd"),
+    PagureService(token="abcd", instance_url="https://src.fedoraproject.org")
+}
+
+

When the mapping key->service-type is not recognised, you can add a type +key to the dictionary and specify the type of the instance. +(It can be either name, hostname or url. The used mapping is same as for +key->service-type.)

+

The provided key is used as an instance_url and passed to the __init__ +method as well.

+

e.g.:

+
get_instances_from_dict({
+    "https://my.gtlb": {"token": "abcd", "type": "gitlab"},
+}) == {GitlabService(token="abcd", instance_url="https://my.gtlb")}
+
+

Args

+
+
instances
+
Mapping from service name/url/hostname to attributes for the +service creation.
+
+

Returns

+

Set of the service instances.

+
+ +Expand source code + +
def get_instances_from_dict(instances: dict) -> set[GitService]:
+    """
+    Load the service instances from the dictionary in the following form:
+
+    - `key`   : hostname, url or name that can be mapped to the service-type
+    - `value` : dictionary with arguments used when creating a new instance of the
+    service (passed to the `__init__` method)
+
+    e.g.:
+    ```py
+    get_instances_from_dict({
+        "github.com": {"token": "abcd"},
+        "pagure": {
+            "token": "abcd",
+            "instance_url": "https://src.fedoraproject.org",
+        },
+    }) == {
+        GithubService(token="abcd"),
+        PagureService(token="abcd", instance_url="https://src.fedoraproject.org")
+    }
+    ```
+
+    When the mapping `key->service-type` is not recognised, you can add a `type`
+    key to the dictionary and specify the type of the instance.
+    (It can be either name, hostname or url. The used mapping is same as for
+    key->service-type.)
+
+    The provided `key` is used as an `instance_url` and passed to the `__init__`
+    method as well.
+
+    e.g.:
+    ```py
+    get_instances_from_dict({
+        "https://my.gtlb": {"token": "abcd", "type": "gitlab"},
+    }) == {GitlabService(token="abcd", instance_url="https://my.gtlb")}
+    ```
+
+    Args:
+        instances: Mapping from service name/url/hostname to attributes for the
+            service creation.
+
+    Returns:
+        Set of the service instances.
+    """
+    services = set()
+    for key, value in instances.items():
+        service_kls = get_service_class_or_none(url=key)
+        if not service_kls:
+            if "type" not in value:
+                raise OgrException(
+                    f"No matching service was found for url '{key}'. "
+                    f"Add the service name as a `type` attribute.",
+                )
+            service_type = value["type"]
+            if service_type not in _SERVICE_MAPPING:
+                raise OgrException(
+                    f"No matching service was found for type '{service_type}'.",
+                )
+
+            service_kls = _SERVICE_MAPPING[service_type]
+            value.setdefault("instance_url", key)
+            del value["type"]
+
+        service_instance = service_kls(**value)
+        services.add(service_instance)
+
+    return services
+
+
+
+def get_project(url, service_mapping_update: Optional[dict[str, type[GitService]]] = None, custom_instances: Optional[collections.abc.Iterable[GitService]] = None, force_custom_instance: bool = True, **kwargs) ‑> GitProject +
+
+

Return the project for the given URL.

+

Args

+
+
url
+
URL of the project, e.g. "https://github.com/packit/ogr".
+
service_mapping_update
+
+

Custom mapping from +service url/hostname (str) to service class.

+

Defaults to no mapping.

+
+
custom_instances
+
+

List of instances that will be +used when creating a project instance.

+

Defaults to None.

+
+
force_custom_instance
+
+

Force picking a Git service from the +custom_instances list, if there is any provided, raise an error if +that is not possible.

+

Defaults to True.

+
+
**kwargs
+
Arguments forwarded to init of the matching service.
+
+

Returns

+

GitProject using the matching implementation.

+
+ +Expand source code + +
def get_project(
+    url,
+    service_mapping_update: Optional[dict[str, type[GitService]]] = None,
+    custom_instances: Optional[Iterable[GitService]] = None,
+    force_custom_instance: bool = True,
+    **kwargs,
+) -> GitProject:
+    """
+    Return the project for the given URL.
+
+    Args:
+        url: URL of the project, e.g. `"https://github.com/packit/ogr"`.
+        service_mapping_update: Custom mapping from
+            service url/hostname (`str`) to service class.
+
+            Defaults to no mapping.
+        custom_instances: List of instances that will be
+            used when creating a project instance.
+
+            Defaults to `None`.
+        force_custom_instance: Force picking a Git service from the
+            `custom_instances` list, if there is any provided, raise an error if
+            that is not possible.
+
+            Defaults to `True`.
+        **kwargs: Arguments forwarded to __init__ of the matching service.
+
+    Returns:
+        `GitProject` using the matching implementation.
+    """
+    mapping = service_mapping_update.copy() if service_mapping_update else {}
+    custom_instances = custom_instances or []
+    for instance in custom_instances:
+        mapping[instance.hostname] = instance.__class__
+
+    kls = get_service_class(url=url, service_mapping_update=mapping)
+    parsed_repo_url = parse_git_repo(url)
+
+    service = None
+    if custom_instances:
+        for service_inst in custom_instances:
+            if (
+                isinstance(service_inst, kls)
+                and service_inst.hostname == parsed_repo_url.hostname
+            ):
+                service = service_inst
+                break
+        else:
+            if force_custom_instance:
+                raise OgrException(
+                    f"Instance of type {kls.__name__} "
+                    f"matching instance url '{url}' was not provided.",
+                )
+    if not service:
+        service = kls(instance_url=parsed_repo_url.get_instance_url(), **kwargs)
+    return service.get_project_from_url(url=url)
+
+
+
+def get_service_class(url: str, service_mapping_update: Optional[dict[str, type[GitService]]] = None) ‑> type[GitService] +
+
+

Get the matching service class from the URL.

+

Args

+
+
url
+
URL of the project, e.g. "https://github.com/packit/ogr".
+
service_mapping_update
+
+

Custom mapping from service url/hostname (str) to service +class.

+

Defaults to None.

+
+
+

Returns

+

Matched class (subclass of GitService).

+
+ +Expand source code + +
def get_service_class(
+    url: str,
+    service_mapping_update: Optional[dict[str, type[GitService]]] = None,
+) -> type[GitService]:
+    """
+    Get the matching service class from the URL.
+
+    Args:
+        url: URL of the project, e.g. `"https://github.com/packit/ogr"`.
+        service_mapping_update: Custom mapping from service url/hostname (str) to service
+            class.
+
+            Defaults to `None`.
+
+    Returns:
+        Matched class (subclass of `GitService`).
+    """
+    service_kls = get_service_class_or_none(
+        url=url,
+        service_mapping_update=service_mapping_update,
+    )
+    if service_kls:
+        return service_kls
+    raise OgrException("No matching service was found.")
+
+
+
+def get_service_class_or_none(url: str, service_mapping_update: Optional[dict[str, type[GitService]]] = None) ‑> Optional[type[GitService]] +
+
+

Get the matching service class from the URL.

+

Args

+
+
url
+
URL of the project, e.g. "https://github.com/packit/ogr".
+
service_mapping_update
+
+

Custom mapping from service url/hostname (str) to service +class.

+

Defaults to None.

+
+
+

Returns

+

Matched class (subclass of GitService) or None.

+
+ +Expand source code + +
def get_service_class_or_none(
+    url: str,
+    service_mapping_update: Optional[dict[str, type[GitService]]] = None,
+) -> Optional[type[GitService]]:
+    """
+    Get the matching service class from the URL.
+
+    Args:
+        url: URL of the project, e.g. `"https://github.com/packit/ogr"`.
+        service_mapping_update: Custom mapping from service url/hostname (`str`) to service
+            class.
+
+            Defaults to `None`.
+
+    Returns:
+        Matched class (subclass of `GitService`) or `None`.
+    """
+    mapping = {}
+    mapping.update(_SERVICE_MAPPING)
+    if service_mapping_update:
+        mapping.update(service_mapping_update)
+
+    parsed_url = parse_git_repo(url)
+    for service, service_kls in mapping.items():
+        if parse_git_repo(service).hostname in parsed_url.hostname:
+            return service_kls
+
+    return None
+
+
+
+def use_for_service(service: str) +
+
+

Class decorator that adds the class to the service mapping.

+

When the project url contains the service as a substring, +this implementation will be used to initialize the project.

+

When using this decorator, be sure that your class is initialized. +(Add the import to ogr/__init__.py)

+

Usage:

+
@use_for_service("github.com")
+class GithubService(BaseGitService):
+    pass
+
+@use_for_service("pagure.io")
+@use_for_service("src.fedoraproject.org")
+class PagureService(BaseGitService):
+    pass
+
+

Args

+
+
service
+
URL of the service.
+
+

Returns

+

Decorator.

+
+ +Expand source code + +
def use_for_service(service: str, _func=None):
+    """
+    Class decorator that adds the class to the service mapping.
+
+    When the project url contains the `service` as a substring,
+    this implementation will be used to initialize the project.
+
+    When using this decorator, be sure that your class is initialized.
+    (Add the import to `ogr/__init__.py`)
+
+    Usage:
+    ```py
+    @use_for_service("github.com")
+    class GithubService(BaseGitService):
+        pass
+
+    @use_for_service("pagure.io")
+    @use_for_service("src.fedoraproject.org")
+    class PagureService(BaseGitService):
+        pass
+    ```
+
+    Args:
+        service: URL of the service.
+
+    Returns:
+        Decorator.
+    """
+
+    def decorator_cover(func):
+        @functools.wraps(func)
+        def covered_func(kls: type[GitService]):
+            _SERVICE_MAPPING[service] = kls
+            return kls
+
+        return covered_func
+
+    return decorator_cover(_func)
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..a15bff02 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,1863 @@ + + + + + + +ogr API documentation + + + + + + + + + + + +
+
+
+

Package ogr

+
+
+

Module providing one api for multiple git services (github/gitlab/pagure)

+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+"""
+Module providing one api for multiple git services (github/gitlab/pagure)
+"""
+
+import contextlib
+from importlib.metadata import PackageNotFoundError, distribution
+
+from ogr.abstract import AuthMethod
+from ogr.factory import (
+    get_instances_from_dict,
+    get_project,
+    get_service_class,
+    get_service_class_or_none,
+)
+from ogr.services.github import GithubService
+from ogr.services.gitlab import GitlabService
+from ogr.services.pagure import PagureService
+
+with contextlib.suppress(PackageNotFoundError):
+    __version__ = distribution(__name__).version
+
+__all__ = [
+    GithubService.__name__,
+    PagureService.__name__,
+    GitlabService.__name__,
+    AuthMethod.__name__,
+    get_project.__name__,
+    get_service_class.__name__,
+    get_service_class_or_none.__name__,
+    get_instances_from_dict.__name__,
+]
+
+
+
+

Sub-modules

+
+
ogr.abstract
+
+
+
+
ogr.constant
+
+
+
+
ogr.deprecation
+
+
+
+
ogr.exceptions
+
+
+
+
ogr.factory
+
+
+
+
ogr.parsing
+
+
+
+
ogr.read_only
+
+
+
+
ogr.services
+
+
+
+
ogr.utils
+
+
+
+
+
+
+
+
+

Functions

+
+
+def get_instances_from_dict(instances: dict) ‑> set[GitService] +
+
+

Load the service instances from the dictionary in the following form:

+
    +
  • key +: hostname, url or name that can be mapped to the service-type
  • +
  • value : dictionary with arguments used when creating a new instance of the +service (passed to the __init__ method)
  • +
+

e.g.:

+
get_instances_from_dict({
+    "github.com": {"token": "abcd"},
+    "pagure": {
+        "token": "abcd",
+        "instance_url": "https://src.fedoraproject.org",
+    },
+}) == {
+    GithubService(token="abcd"),
+    PagureService(token="abcd", instance_url="https://src.fedoraproject.org")
+}
+
+

When the mapping key->service-type is not recognised, you can add a type +key to the dictionary and specify the type of the instance. +(It can be either name, hostname or url. The used mapping is same as for +key->service-type.)

+

The provided key is used as an instance_url and passed to the __init__ +method as well.

+

e.g.:

+
get_instances_from_dict({
+    "https://my.gtlb": {"token": "abcd", "type": "gitlab"},
+}) == {GitlabService(token="abcd", instance_url="https://my.gtlb")}
+
+

Args

+
+
instances
+
Mapping from service name/url/hostname to attributes for the +service creation.
+
+

Returns

+

Set of the service instances.

+
+ +Expand source code + +
def get_instances_from_dict(instances: dict) -> set[GitService]:
+    """
+    Load the service instances from the dictionary in the following form:
+
+    - `key`   : hostname, url or name that can be mapped to the service-type
+    - `value` : dictionary with arguments used when creating a new instance of the
+    service (passed to the `__init__` method)
+
+    e.g.:
+    ```py
+    get_instances_from_dict({
+        "github.com": {"token": "abcd"},
+        "pagure": {
+            "token": "abcd",
+            "instance_url": "https://src.fedoraproject.org",
+        },
+    }) == {
+        GithubService(token="abcd"),
+        PagureService(token="abcd", instance_url="https://src.fedoraproject.org")
+    }
+    ```
+
+    When the mapping `key->service-type` is not recognised, you can add a `type`
+    key to the dictionary and specify the type of the instance.
+    (It can be either name, hostname or url. The used mapping is same as for
+    key->service-type.)
+
+    The provided `key` is used as an `instance_url` and passed to the `__init__`
+    method as well.
+
+    e.g.:
+    ```py
+    get_instances_from_dict({
+        "https://my.gtlb": {"token": "abcd", "type": "gitlab"},
+    }) == {GitlabService(token="abcd", instance_url="https://my.gtlb")}
+    ```
+
+    Args:
+        instances: Mapping from service name/url/hostname to attributes for the
+            service creation.
+
+    Returns:
+        Set of the service instances.
+    """
+    services = set()
+    for key, value in instances.items():
+        service_kls = get_service_class_or_none(url=key)
+        if not service_kls:
+            if "type" not in value:
+                raise OgrException(
+                    f"No matching service was found for url '{key}'. "
+                    f"Add the service name as a `type` attribute.",
+                )
+            service_type = value["type"]
+            if service_type not in _SERVICE_MAPPING:
+                raise OgrException(
+                    f"No matching service was found for type '{service_type}'.",
+                )
+
+            service_kls = _SERVICE_MAPPING[service_type]
+            value.setdefault("instance_url", key)
+            del value["type"]
+
+        service_instance = service_kls(**value)
+        services.add(service_instance)
+
+    return services
+
+
+
+def get_project(url, service_mapping_update: Optional[dict[str, type[GitService]]] = None, custom_instances: Optional[collections.abc.Iterable[GitService]] = None, force_custom_instance: bool = True, **kwargs) ‑> GitProject +
+
+

Return the project for the given URL.

+

Args

+
+
url
+
URL of the project, e.g. "https://github.com/packit/ogr".
+
service_mapping_update
+
+

Custom mapping from +service url/hostname (str) to service class.

+

Defaults to no mapping.

+
+
custom_instances
+
+

List of instances that will be +used when creating a project instance.

+

Defaults to None.

+
+
force_custom_instance
+
+

Force picking a Git service from the +custom_instances list, if there is any provided, raise an error if +that is not possible.

+

Defaults to True.

+
+
**kwargs
+
Arguments forwarded to init of the matching service.
+
+

Returns

+

GitProject using the matching implementation.

+
+ +Expand source code + +
def get_project(
+    url,
+    service_mapping_update: Optional[dict[str, type[GitService]]] = None,
+    custom_instances: Optional[Iterable[GitService]] = None,
+    force_custom_instance: bool = True,
+    **kwargs,
+) -> GitProject:
+    """
+    Return the project for the given URL.
+
+    Args:
+        url: URL of the project, e.g. `"https://github.com/packit/ogr"`.
+        service_mapping_update: Custom mapping from
+            service url/hostname (`str`) to service class.
+
+            Defaults to no mapping.
+        custom_instances: List of instances that will be
+            used when creating a project instance.
+
+            Defaults to `None`.
+        force_custom_instance: Force picking a Git service from the
+            `custom_instances` list, if there is any provided, raise an error if
+            that is not possible.
+
+            Defaults to `True`.
+        **kwargs: Arguments forwarded to __init__ of the matching service.
+
+    Returns:
+        `GitProject` using the matching implementation.
+    """
+    mapping = service_mapping_update.copy() if service_mapping_update else {}
+    custom_instances = custom_instances or []
+    for instance in custom_instances:
+        mapping[instance.hostname] = instance.__class__
+
+    kls = get_service_class(url=url, service_mapping_update=mapping)
+    parsed_repo_url = parse_git_repo(url)
+
+    service = None
+    if custom_instances:
+        for service_inst in custom_instances:
+            if (
+                isinstance(service_inst, kls)
+                and service_inst.hostname == parsed_repo_url.hostname
+            ):
+                service = service_inst
+                break
+        else:
+            if force_custom_instance:
+                raise OgrException(
+                    f"Instance of type {kls.__name__} "
+                    f"matching instance url '{url}' was not provided.",
+                )
+    if not service:
+        service = kls(instance_url=parsed_repo_url.get_instance_url(), **kwargs)
+    return service.get_project_from_url(url=url)
+
+
+
+def get_service_class(url: str, service_mapping_update: Optional[dict[str, type[GitService]]] = None) ‑> type[GitService] +
+
+

Get the matching service class from the URL.

+

Args

+
+
url
+
URL of the project, e.g. "https://github.com/packit/ogr".
+
service_mapping_update
+
+

Custom mapping from service url/hostname (str) to service +class.

+

Defaults to None.

+
+
+

Returns

+

Matched class (subclass of GitService).

+
+ +Expand source code + +
def get_service_class(
+    url: str,
+    service_mapping_update: Optional[dict[str, type[GitService]]] = None,
+) -> type[GitService]:
+    """
+    Get the matching service class from the URL.
+
+    Args:
+        url: URL of the project, e.g. `"https://github.com/packit/ogr"`.
+        service_mapping_update: Custom mapping from service url/hostname (str) to service
+            class.
+
+            Defaults to `None`.
+
+    Returns:
+        Matched class (subclass of `GitService`).
+    """
+    service_kls = get_service_class_or_none(
+        url=url,
+        service_mapping_update=service_mapping_update,
+    )
+    if service_kls:
+        return service_kls
+    raise OgrException("No matching service was found.")
+
+
+
+def get_service_class_or_none(url: str, service_mapping_update: Optional[dict[str, type[GitService]]] = None) ‑> Optional[type[GitService]] +
+
+

Get the matching service class from the URL.

+

Args

+
+
url
+
URL of the project, e.g. "https://github.com/packit/ogr".
+
service_mapping_update
+
+

Custom mapping from service url/hostname (str) to service +class.

+

Defaults to None.

+
+
+

Returns

+

Matched class (subclass of GitService) or None.

+
+ +Expand source code + +
def get_service_class_or_none(
+    url: str,
+    service_mapping_update: Optional[dict[str, type[GitService]]] = None,
+) -> Optional[type[GitService]]:
+    """
+    Get the matching service class from the URL.
+
+    Args:
+        url: URL of the project, e.g. `"https://github.com/packit/ogr"`.
+        service_mapping_update: Custom mapping from service url/hostname (`str`) to service
+            class.
+
+            Defaults to `None`.
+
+    Returns:
+        Matched class (subclass of `GitService`) or `None`.
+    """
+    mapping = {}
+    mapping.update(_SERVICE_MAPPING)
+    if service_mapping_update:
+        mapping.update(service_mapping_update)
+
+    parsed_url = parse_git_repo(url)
+    for service, service_kls in mapping.items():
+        if parse_git_repo(service).hostname in parsed_url.hostname:
+            return service_kls
+
+    return None
+
+
+
+
+
+

Classes

+
+
+class AuthMethod +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

An enumeration.

+
+ +Expand source code + +
class AuthMethod(str, Enum):
+    tokman = "tokman"
+    github_app = "github_app"
+    token = "token"
+
+

Ancestors

+
    +
  • builtins.str
  • +
  • enum.Enum
  • +
+

Class variables

+
+
var github_app
+
+
+
+
var token
+
+
+
+
var tokman
+
+
+
+
+
+
+class GithubService +(token=None, read_only=False, github_app_id: Optional[str] = None, github_app_private_key: Optional[str] = None, github_app_private_key_path: Optional[str] = None, tokman_instance_url: Optional[str] = None, github_authentication: GithubAuthentication = None, max_retries: Union[int, urllib3.util.retry.Retry] = 0, **kwargs) +
+
+

Attributes

+
+
instance_url : str
+
URL of the git forge instance.
+
+

If multiple authentication methods are provided, they are prioritised: +1. Tokman +2. GithubApp +3. TokenAuthentication (which is also default one, that works without specified token)

+
+ +Expand source code + +
@use_for_service("github.com")
+class GithubService(BaseGitService):
+    # class parameter could be used to mock Github class api
+    github_class: type[github.Github]
+    instance_url = "https://github.com"
+
+    def __init__(
+        self,
+        token=None,
+        read_only=False,
+        github_app_id: Optional[str] = None,
+        github_app_private_key: Optional[str] = None,
+        github_app_private_key_path: Optional[str] = None,
+        tokman_instance_url: Optional[str] = None,
+        github_authentication: GithubAuthentication = None,
+        max_retries: Union[int, Retry] = 0,
+        **kwargs,
+    ):
+        """
+        If multiple authentication methods are provided, they are prioritised:
+            1. Tokman
+            2. GithubApp
+            3. TokenAuthentication (which is also default one, that works without specified token)
+        """
+        super().__init__()
+        self.read_only = read_only
+        self._default_auth_method = github_authentication
+        self._other_auth_method: GithubAuthentication = None
+        self._auth_methods: dict[AuthMethod, GithubAuthentication] = {}
+
+        if isinstance(max_retries, Retry):
+            self._max_retries = max_retries
+        else:
+            self._max_retries = Retry(
+                total=int(max_retries),
+                read=0,
+                # Retry mechanism active for these HTTP methods:
+                allowed_methods=["DELETE", "GET", "PATCH", "POST", "PUT"],
+                # Only retry on following HTTP status codes
+                status_forcelist=[500, 503, 403, 401],
+                raise_on_status=False,
+            )
+
+        if not self._default_auth_method:
+            self.__set_authentication(
+                token=token,
+                github_app_id=github_app_id,
+                github_app_private_key=github_app_private_key,
+                github_app_private_key_path=github_app_private_key_path,
+                tokman_instance_url=tokman_instance_url,
+                max_retries=self._max_retries,
+            )
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    def __set_authentication(self, **kwargs):
+        auth_methods = [
+            (Tokman, AuthMethod.tokman),
+            (GithubApp, AuthMethod.github_app),
+            (TokenAuthentication, AuthMethod.token),
+        ]
+        for auth_class, auth_name in auth_methods:
+            auth_inst = auth_class.try_create(**kwargs)
+            self._auth_methods[auth_name] = auth_inst
+            if not self._default_auth_method:
+                self._default_auth_method = auth_inst
+
+        return None if self._default_auth_method else TokenAuthentication(None)
+
+    def set_auth_method(self, method: AuthMethod):
+        if self._auth_methods[method]:
+            logger.info("Forced Github auth method to %s", method)
+            self._other_auth_method = self._auth_methods[method]
+        else:
+            raise GithubAPIException(
+                f"Choosen authentication method ({method}) is not available",
+            )
+
+    def reset_auth_method(self):
+        logger.info("Reset Github auth method to the default")
+        self._other_auth_method = None
+
+    @property
+    def authentication(self):
+        return self._other_auth_method or self._default_auth_method
+
+    @property
+    def github(self):
+        return self.authentication.pygithub_instance
+
+    def __str__(self) -> str:
+        readonly_str = ", read_only=True" if self.read_only else ""
+        arguments = f", github_authentication={self.authentication!s}{readonly_str}"
+
+        if arguments:
+            # remove the first '- '
+            arguments = arguments[2:]
+
+        return f"GithubService({arguments})"
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, GithubService):
+            return False
+
+        return (
+            self.read_only == o.read_only  # type: ignore
+            and self.authentication == o.authentication  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(
+        self,
+        repo=None,
+        namespace=None,
+        is_fork=False,
+        **kwargs,
+    ) -> "GithubProject":
+        if is_fork:
+            namespace = self.user.get_username()
+        return GithubProject(
+            repo=repo,
+            namespace=namespace,
+            service=self,
+            read_only=self.read_only,
+            **kwargs,
+        )
+
+    def get_project_from_github_repository(
+        self,
+        github_repo: PyGithubRepository.Repository,
+    ) -> "GithubProject":
+        return GithubProject(
+            repo=github_repo.name,
+            namespace=github_repo.owner.login,
+            github_repo=github_repo,
+            service=self,
+            read_only=self.read_only,
+        )
+
+    @property
+    def user(self) -> GitUser:
+        return GithubUser(service=self)
+
+    def change_token(self, new_token: str) -> None:
+        self._default_auth_method = TokenAuthentication(new_token)
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "GithubProject":
+        if namespace:
+            try:
+                owner = self.github.get_organization(namespace)
+            except UnknownObjectException as ex:
+                raise GithubAPIException(f"Group {namespace} not found.") from ex
+        else:
+            owner = self.github.get_user()
+
+        try:
+            new_repo = owner.create_repo(
+                name=repo,
+                description=description if description else github.GithubObject.NotSet,
+            )
+        except github.GithubException as ex:
+            raise GithubAPIException("Project creation failed") from ex
+        return GithubProject(
+            repo=repo,
+            namespace=namespace or owner.login,
+            service=self,
+            github_repo=new_repo,
+        )
+
+    def get_pygithub_instance(self, namespace: str, repo: str) -> PyGithubInstance:
+        token = self.authentication.get_token(namespace, repo)
+        return PyGithubInstance(login_or_token=token, retry=self._max_retries)
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        search_query = ""
+
+        if user:
+            search_query += f"user:{user}"
+
+        if language:
+            search_query += f" language:{language}"
+
+        projects: list[GitProject]
+        projects = [
+            GithubProject(
+                repo=repo.name,
+                namespace=repo.owner.login,
+                github_repo=repo,
+                service=self,
+            )
+            for repo in self.github.search_repositories(search_query, order="asc")
+        ]
+
+        if search_pattern:
+            projects = [
+                project
+                for project in projects
+                if re.search(search_pattern, project.repo)
+            ]
+
+        return projects
+
+

Ancestors

+ +

Class variables

+
+
var github_class : type[github.MainClass.Github]
+
+
+
+
var instance_url : Optional[str]
+
+
+
+
+

Instance variables

+
+
var authentication
+
+
+
+ +Expand source code + +
@property
+def authentication(self):
+    return self._other_auth_method or self._default_auth_method
+
+
+
var github
+
+
+
+ +Expand source code + +
@property
+def github(self):
+    return self.authentication.pygithub_instance
+
+
+
+

Methods

+
+
+def get_project_from_github_repository(self, github_repo: github.Repository.Repository) ‑> GithubProject +
+
+
+
+ +Expand source code + +
def get_project_from_github_repository(
+    self,
+    github_repo: PyGithubRepository.Repository,
+) -> "GithubProject":
+    return GithubProject(
+        repo=github_repo.name,
+        namespace=github_repo.owner.login,
+        github_repo=github_repo,
+        service=self,
+        read_only=self.read_only,
+    )
+
+
+
+def get_pygithub_instance(self, namespace: str, repo: str) ‑> github.MainClass.Github +
+
+
+
+ +Expand source code + +
def get_pygithub_instance(self, namespace: str, repo: str) -> PyGithubInstance:
+    token = self.authentication.get_token(namespace, repo)
+    return PyGithubInstance(login_or_token=token, retry=self._max_retries)
+
+
+
+

Inherited members

+ +
+
+class GitlabService +(token=None, instance_url=None, ssl_verify=True, **kwargs) +
+
+

Attributes

+
+
instance_url : str
+
URL of the git forge instance.
+
+
+ +Expand source code + +
@use_for_service("gitlab")  # anything containing a gitlab word in hostname
+# + list of community-hosted instances based on the following list
+# https://wiki.p2pfoundation.net/List_of_Community-Hosted_GitLab_Instances
+@use_for_service("salsa.debian.org")
+@use_for_service("git.fosscommunity.in")
+@use_for_service("framagit.org")
+@use_for_service("dev.gajim.org")
+@use_for_service("git.coop")
+@use_for_service("lab.libreho.st")
+@use_for_service("git.linux-kernel.at")
+@use_for_service("git.pleroma.social")
+@use_for_service("git.silence.dev")
+@use_for_service("code.videolan.org")
+@use_for_service("source.puri.sm")
+class GitlabService(BaseGitService):
+    name = "gitlab"
+
+    def __init__(self, token=None, instance_url=None, ssl_verify=True, **kwargs):
+        super().__init__(token=token)
+        self.instance_url = instance_url or "https://gitlab.com"
+        self.token = token
+        self.ssl_verify = ssl_verify
+        self._gitlab_instance = None
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    @property
+    def gitlab_instance(self) -> gitlab.Gitlab:
+        if not self._gitlab_instance:
+            self._gitlab_instance = gitlab.Gitlab(
+                url=self.instance_url,
+                private_token=self.token,
+                ssl_verify=self.ssl_verify,
+            )
+            if self.token:
+                self._gitlab_instance.auth()
+        return self._gitlab_instance
+
+    @property
+    def user(self) -> GitUser:
+        return GitlabUser(service=self)
+
+    def __str__(self) -> str:
+        token_str = (
+            f", token='{self.token[:1]}***{self.token[-1:]}'" if self.token else ""
+        )
+        ssl_str = ", ssl_verify=False" if not self.ssl_verify else ""
+        return (
+            f"GitlabService(instance_url='{self.instance_url}'"
+            f"{token_str}"
+            f"{ssl_str})"
+        )
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, GitlabService):
+            return False
+
+        return (
+            self.token == o.token  # type: ignore
+            and self.instance_url == o.instance_url  # type: ignore
+            and self.ssl_verify == o.ssl_verify  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(
+        self,
+        repo=None,
+        namespace=None,
+        is_fork=False,
+        **kwargs,
+    ) -> "GitlabProject":
+        if is_fork:
+            namespace = self.user.get_username()
+        return GitlabProject(repo=repo, namespace=namespace, service=self, **kwargs)
+
+    def get_project_from_project_id(self, iid: int) -> "GitlabProject":
+        gitlab_repo = self.gitlab_instance.projects.get(iid)
+        return GitlabProject(
+            repo=gitlab_repo.attributes["path"],
+            namespace=gitlab_repo.attributes["namespace"]["full_path"],
+            service=self,
+            gitlab_repo=gitlab_repo,
+        )
+
+    def change_token(self, new_token: str) -> None:
+        self.token = new_token
+        self._gitlab_instance = None
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "GitlabProject":
+        data = {"name": repo}
+        if namespace:
+            try:
+                group = self.gitlab_instance.groups.get(namespace)
+            except gitlab.GitlabGetError as ex:
+                raise GitlabAPIException(f"Group {namespace} not found.") from ex
+            data["namespace_id"] = group.id
+
+        if description:
+            data["description"] = description
+        try:
+            new_project = self.gitlab_instance.projects.create(data)
+        except gitlab.GitlabCreateError as ex:
+            raise GitlabAPIException("Project already exists") from ex
+        return GitlabProject(
+            repo=repo,
+            namespace=namespace,
+            service=self,
+            gitlab_repo=new_project,
+        )
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        if namespace:
+            group = self.gitlab_instance.groups.get(namespace)
+            projects = group.projects.list(all=True)
+        elif user:
+            user_object = self.gitlab_instance.users.list(username=user)[0]
+            projects = user_object.projects.list(all=True)
+        else:
+            raise OperationNotSupported
+
+        if language:
+            # group.projects.list gives us a GroupProject instance
+            # in order to be able to filter by language we need Project instance
+            projects_to_convert = [
+                self.gitlab_instance.projects.get(item.attributes["id"])
+                for item in projects
+                if language
+                in self.gitlab_instance.projects.get(item.attributes["id"]).languages()
+            ]
+        else:
+            projects_to_convert = projects
+        return [
+            GitlabProject(
+                repo=project.attributes["path"],
+                namespace=project.attributes["namespace"]["full_path"],
+                service=self,
+            )
+            for project in projects_to_convert
+        ]
+
+

Ancestors

+ +

Class variables

+
+
var name
+
+
+
+
+

Instance variables

+
+
var gitlab_instance : gitlab.client.Gitlab
+
+
+
+ +Expand source code + +
@property
+def gitlab_instance(self) -> gitlab.Gitlab:
+    if not self._gitlab_instance:
+        self._gitlab_instance = gitlab.Gitlab(
+            url=self.instance_url,
+            private_token=self.token,
+            ssl_verify=self.ssl_verify,
+        )
+        if self.token:
+            self._gitlab_instance.auth()
+    return self._gitlab_instance
+
+
+
+

Methods

+
+
+def get_project_from_project_id(self, iid: int) ‑> GitlabProject +
+
+
+
+ +Expand source code + +
def get_project_from_project_id(self, iid: int) -> "GitlabProject":
+    gitlab_repo = self.gitlab_instance.projects.get(iid)
+    return GitlabProject(
+        repo=gitlab_repo.attributes["path"],
+        namespace=gitlab_repo.attributes["namespace"]["full_path"],
+        service=self,
+        gitlab_repo=gitlab_repo,
+    )
+
+
+
+

Inherited members

+ +
+
+class PagureService +(token: Optional[str] = None, instance_url: str = 'https://src.fedoraproject.org', read_only: bool = False, insecure: bool = False, max_retries: Union[int, urllib3.util.retry.Retry] = 5, **kwargs) +
+
+

Attributes

+
+
instance_url : str
+
URL of the git forge instance.
+
+
+ +Expand source code + +
@use_for_service("pagure")
+@use_for_service("src.fedoraproject.org")
+@use_for_service("src.stg.fedoraproject.org")
+@use_for_service("pkgs.fedoraproject.org")
+@use_for_service("pkgs.stg.fedoraproject.org")
+@use_for_service("git.centos.org")
+@use_for_service("git.stg.centos.org")
+class PagureService(BaseGitService):
+    def __init__(
+        self,
+        token: Optional[str] = None,
+        instance_url: str = "https://src.fedoraproject.org",
+        read_only: bool = False,
+        insecure: bool = False,
+        max_retries: Union[int, urllib3.util.Retry] = 5,
+        **kwargs,
+    ) -> None:
+        super().__init__()
+        self.instance_url = instance_url
+        self._token = token
+        self.read_only = read_only
+
+        self.session = requests.session()
+
+        adapter = requests.adapters.HTTPAdapter(max_retries=max_retries)
+
+        self.insecure = insecure
+        if self.insecure:
+            self.session.mount("http://", adapter)
+        else:
+            self.session.mount("https://", adapter)
+
+        self.header = {"Authorization": "token " + self._token} if self._token else {}
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    def __str__(self) -> str:
+        token_str = (
+            f", token='{self._token[:1]}***{self._token[-1:]}'" if self._token else ""
+        )
+        insecure_str = ", insecure=True" if self.insecure else ""
+        readonly_str = ", read_only=True" if self.read_only else ""
+
+        return (
+            f"PagureService(instance_url='{self.instance_url}'"
+            f"{token_str}"
+            f"{readonly_str}"
+            f"{insecure_str})"
+        )
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, PagureService):
+            return False
+
+        return (
+            self._token == o._token  # type: ignore
+            and self.read_only == o.read_only  # type: ignore
+            and self.instance_url == o.instance_url  # type: ignore
+            and self.insecure == o.insecure  # type: ignore
+            and self.header == o.header  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(self, **kwargs) -> "PagureProject":
+        if "username" in kwargs:
+            return PagureProject(service=self, **kwargs)
+
+        return PagureProject(
+            service=self,
+            username=self.user.get_username(),
+            **kwargs,
+        )
+
+    def get_project_from_url(self, url: str) -> "PagureProject":
+        repo_url = parse_git_repo(potential_url=url)
+        if not repo_url:
+            raise OgrException(f"Cannot parse project url: '{url}'")
+
+        if not repo_url.is_fork:
+            repo_url.username = None
+
+        return self.get_project(
+            repo=repo_url.repo,
+            namespace=repo_url.namespace,
+            is_fork=repo_url.is_fork,
+            username=repo_url.username,
+        )
+
+    @property
+    def user(self) -> "PagureUser":
+        return PagureUser(service=self)
+
+    def call_api(
+        self,
+        url: str,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data=None,
+    ) -> dict:
+        """
+        Call API endpoint.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            Dictionary representing response.
+
+        Raises:
+            PagureAPIException, if error occurs.
+        """
+        response = self.call_api_raw(url=url, method=method, params=params, data=data)
+
+        if response.status_code == 404:
+            error_msg = (
+                response.json_content["error"]
+                if response.json_content and "error" in response.json_content
+                else None
+            )
+            raise PagureAPIException(
+                f"Page '{url}' not found when calling Pagure API.",
+                pagure_error=error_msg,
+                response_code=response.status_code,
+            )
+
+        if not response.json_content:
+            logger.debug(response.content)
+            raise PagureAPIException(
+                "Error while decoding JSON: {0}",
+                response_code=response.status_code,
+            )
+
+        if not response.ok:
+            logger.error(response.json_content)
+            if "error" in response.json_content:
+                error_msg = response.json_content["error"]
+                error_msg_ext = response.json_content.get("errors", "")
+                msg = f"Pagure API returned an error when calling '{url}': {error_msg}"
+                if error_msg_ext:
+                    msg += f" - {error_msg_ext}"
+                raise PagureAPIException(
+                    msg,
+                    pagure_error=error_msg,
+                    pagure_response=response.json_content,
+                    response_code=response.status_code,
+                )
+            raise PagureAPIException(
+                f"Problem with Pagure API when calling '{url}'",
+                response_code=response.status_code,
+            )
+
+        return response.json_content
+
+    def call_api_raw(
+        self,
+        url: str,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data=None,
+    ):
+        """
+        Call API endpoint and returns raw response.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            `RequestResponse` object that represents the response from the API
+            endpoint.
+        """
+
+        method = method or "GET"
+        try:
+            response = self.get_raw_request(
+                method=method,
+                url=url,
+                params=params,
+                data=data,
+            )
+
+        except requests.exceptions.ConnectionError as er:
+            logger.error(er)
+            raise OgrNetworkError(f"Cannot connect to url: '{url}'.") from er
+
+        if response.status_code >= 500:
+            raise GitForgeInternalError(
+                f"Pagure API returned {response.status_code} status for `{url}`"
+                f" with reason: `{response.reason}`",
+            )
+
+        return response
+
+    def get_raw_request(
+        self,
+        url,
+        method="GET",
+        params=None,
+        data=None,
+        header=None,
+    ) -> RequestResponse:
+        """
+        Call API endpoint and wrap the response in `RequestResponse` type.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+
+                Defaults to `"GET"`.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+            header: Header of the HTTP request.
+
+        Returns:
+            `RequestResponse` object representing the response.
+
+        Raises:
+            ValueError, if JSON cannot be retrieved.
+        """
+
+        response = self.session.request(
+            method=method,
+            url=url,
+            params=params,
+            headers=header or self.header,
+            data=data,
+            verify=not self.insecure,
+        )
+
+        json_output = None
+        try:
+            json_output = response.json()
+        except ValueError:
+            logger.debug(response.text)
+
+        return RequestResponse(
+            status_code=response.status_code,
+            ok=response.ok,
+            content=response.content,
+            json=json_output,
+            reason=response.reason,
+        )
+
+    @property
+    def api_url(self):
+        """URL to the Pagure API."""
+        return f"{self.instance_url}/api/0/"
+
+    def get_api_url(self, *args, add_api_endpoint_part: bool = True) -> str:
+        """
+        Get a URL from its parts.
+
+        Args:
+            *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+            add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+                Defaults to `True`.
+
+        Returns:
+            String
+        """
+        args_list: list[str] = []
+
+        args_list += filter(lambda x: x is not None, args)
+
+        if add_api_endpoint_part:
+            return self.api_url + "/".join(args_list)
+        return f"{self.instance_url}/" + "/".join(args_list)
+
+    def get_api_version(self) -> str:
+        """
+        Returns:
+            Version of the Pagure API.
+        """
+        request_url = self.get_api_url("version")
+        return_value = self.call_api(request_url)
+        return return_value["version"]
+
+    def get_error_codes(self):
+        """
+        Returns:
+            Dictionary with all error codes.
+        """
+        request_url = self.get_api_url("error_codes")
+        return self.call_api(request_url)
+
+    def change_token(self, token: str):
+        self._token = token
+        self.header = {"Authorization": "token " + self._token}
+
+    def __handle_project_create_fail(
+        self,
+        exception: PagureAPIException,
+        namespace: str,
+    ) -> None:
+        if (
+            exception.pagure_response
+            and exception.pagure_response["errors"]["namespace"][0]
+            == "Not a valid choice"
+        ):
+            request_url = self.get_api_url("group", namespace)
+
+            try:
+                self.call_api(request_url, data={"projects": False})
+            except PagureAPIException as ex:
+                raise OgrException(f"Namespace doesn't exist ({namespace}).") from ex
+
+            raise OgrException(
+                "Cannot create project in given namespace (permissions).",
+            )
+
+        raise exception
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> PagureProject:
+        request_url = self.get_api_url("new")
+
+        parameters = {"name": repo, "description": description, "wait": True}
+        if not description:
+            parameters["description"] = repo
+        if namespace:
+            parameters["namespace"] = namespace
+
+        try:
+            self.call_api(request_url, "POST", data=parameters)
+        except PagureAPIException as ex:
+            self.__handle_project_create_fail(ex, namespace)
+        return PagureProject(repo=repo, namespace=namespace, service=self)
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        raise OperationNotSupported
+
+

Ancestors

+ +

Instance variables

+
+
var api_url
+
+

URL to the Pagure API.

+
+ +Expand source code + +
@property
+def api_url(self):
+    """URL to the Pagure API."""
+    return f"{self.instance_url}/api/0/"
+
+
+
+

Methods

+
+
+def call_api(self, url: str, method: Optional[str] = None, params: Optional[dict] = None, data=None) ‑> dict +
+
+

Call API endpoint.

+

Args

+
+
url
+
URL to be called.
+
method
+
Method of the HTTP request, e.g. "GET", "POST", etc.
+
params
+
HTTP(S) query parameters in form of a dictionary.
+
data
+
Data to be sent in form of a dictionary.
+
+

Returns

+

Dictionary representing response.

+

Raises

+

PagureAPIException, if error occurs.

+
+ +Expand source code + +
def call_api(
+    self,
+    url: str,
+    method: Optional[str] = None,
+    params: Optional[dict] = None,
+    data=None,
+) -> dict:
+    """
+    Call API endpoint.
+
+    Args:
+        url: URL to be called.
+        method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+        params: HTTP(S) query parameters in form of a dictionary.
+        data: Data to be sent in form of a dictionary.
+
+    Returns:
+        Dictionary representing response.
+
+    Raises:
+        PagureAPIException, if error occurs.
+    """
+    response = self.call_api_raw(url=url, method=method, params=params, data=data)
+
+    if response.status_code == 404:
+        error_msg = (
+            response.json_content["error"]
+            if response.json_content and "error" in response.json_content
+            else None
+        )
+        raise PagureAPIException(
+            f"Page '{url}' not found when calling Pagure API.",
+            pagure_error=error_msg,
+            response_code=response.status_code,
+        )
+
+    if not response.json_content:
+        logger.debug(response.content)
+        raise PagureAPIException(
+            "Error while decoding JSON: {0}",
+            response_code=response.status_code,
+        )
+
+    if not response.ok:
+        logger.error(response.json_content)
+        if "error" in response.json_content:
+            error_msg = response.json_content["error"]
+            error_msg_ext = response.json_content.get("errors", "")
+            msg = f"Pagure API returned an error when calling '{url}': {error_msg}"
+            if error_msg_ext:
+                msg += f" - {error_msg_ext}"
+            raise PagureAPIException(
+                msg,
+                pagure_error=error_msg,
+                pagure_response=response.json_content,
+                response_code=response.status_code,
+            )
+        raise PagureAPIException(
+            f"Problem with Pagure API when calling '{url}'",
+            response_code=response.status_code,
+        )
+
+    return response.json_content
+
+
+
+def call_api_raw(self, url: str, method: Optional[str] = None, params: Optional[dict] = None, data=None) +
+
+

Call API endpoint and returns raw response.

+

Args

+
+
url
+
URL to be called.
+
method
+
Method of the HTTP request, e.g. "GET", "POST", etc.
+
params
+
HTTP(S) query parameters in form of a dictionary.
+
data
+
Data to be sent in form of a dictionary.
+
+

Returns

+

RequestResponse object that represents the response from the API +endpoint.

+
+ +Expand source code + +
def call_api_raw(
+    self,
+    url: str,
+    method: Optional[str] = None,
+    params: Optional[dict] = None,
+    data=None,
+):
+    """
+    Call API endpoint and returns raw response.
+
+    Args:
+        url: URL to be called.
+        method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+        params: HTTP(S) query parameters in form of a dictionary.
+        data: Data to be sent in form of a dictionary.
+
+    Returns:
+        `RequestResponse` object that represents the response from the API
+        endpoint.
+    """
+
+    method = method or "GET"
+    try:
+        response = self.get_raw_request(
+            method=method,
+            url=url,
+            params=params,
+            data=data,
+        )
+
+    except requests.exceptions.ConnectionError as er:
+        logger.error(er)
+        raise OgrNetworkError(f"Cannot connect to url: '{url}'.") from er
+
+    if response.status_code >= 500:
+        raise GitForgeInternalError(
+            f"Pagure API returned {response.status_code} status for `{url}`"
+            f" with reason: `{response.reason}`",
+        )
+
+    return response
+
+
+
+def get_api_url(self, *args, add_api_endpoint_part: bool = True) ‑> str +
+
+

Get a URL from its parts.

+

Args

+
+
*args
+
String parts of the URL, e.g. "a", "b" will call project/a/b
+
add_api_endpoint_part
+
+

Add part with API endpoint (/api/0/).

+

Defaults to True.

+
+
+

Returns

+

String

+
+ +Expand source code + +
def get_api_url(self, *args, add_api_endpoint_part: bool = True) -> str:
+    """
+    Get a URL from its parts.
+
+    Args:
+        *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+        add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+            Defaults to `True`.
+
+    Returns:
+        String
+    """
+    args_list: list[str] = []
+
+    args_list += filter(lambda x: x is not None, args)
+
+    if add_api_endpoint_part:
+        return self.api_url + "/".join(args_list)
+    return f"{self.instance_url}/" + "/".join(args_list)
+
+
+
+def get_api_version(self) ‑> str +
+
+

Returns

+

Version of the Pagure API.

+
+ +Expand source code + +
def get_api_version(self) -> str:
+    """
+    Returns:
+        Version of the Pagure API.
+    """
+    request_url = self.get_api_url("version")
+    return_value = self.call_api(request_url)
+    return return_value["version"]
+
+
+
+def get_error_codes(self) +
+
+

Returns

+

Dictionary with all error codes.

+
+ +Expand source code + +
def get_error_codes(self):
+    """
+    Returns:
+        Dictionary with all error codes.
+    """
+    request_url = self.get_api_url("error_codes")
+    return self.call_api(request_url)
+
+
+
+def get_raw_request(self, url, method='GET', params=None, data=None, header=None) ‑> RequestResponse +
+
+

Call API endpoint and wrap the response in RequestResponse type.

+

Args

+
+
url
+
URL to be called.
+
method
+
+

Method of the HTTP request, e.g. "GET", "POST", etc.

+

Defaults to "GET".

+
+
params
+
HTTP(S) query parameters in form of a dictionary.
+
data
+
Data to be sent in form of a dictionary.
+
header
+
Header of the HTTP request.
+
+

Returns

+

RequestResponse object representing the response.

+

Raises

+

ValueError, if JSON cannot be retrieved.

+
+ +Expand source code + +
def get_raw_request(
+    self,
+    url,
+    method="GET",
+    params=None,
+    data=None,
+    header=None,
+) -> RequestResponse:
+    """
+    Call API endpoint and wrap the response in `RequestResponse` type.
+
+    Args:
+        url: URL to be called.
+        method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+
+            Defaults to `"GET"`.
+        params: HTTP(S) query parameters in form of a dictionary.
+        data: Data to be sent in form of a dictionary.
+        header: Header of the HTTP request.
+
+    Returns:
+        `RequestResponse` object representing the response.
+
+    Raises:
+        ValueError, if JSON cannot be retrieved.
+    """
+
+    response = self.session.request(
+        method=method,
+        url=url,
+        params=params,
+        headers=header or self.header,
+        data=data,
+        verify=not self.insecure,
+    )
+
+    json_output = None
+    try:
+        json_output = response.json()
+    except ValueError:
+        logger.debug(response.text)
+
+    return RequestResponse(
+        status_code=response.status_code,
+        ok=response.ok,
+        content=response.content,
+        json=json_output,
+        reason=response.reason,
+    )
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/parsing.html b/docs/parsing.html new file mode 100644 index 00000000..9dfee746 --- /dev/null +++ b/docs/parsing.html @@ -0,0 +1,796 @@ + + + + + + +ogr.parsing API documentation + + + + + + + + + + + +
+
+
+

Module ogr.parsing

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from typing import Optional
+from urllib.parse import ParseResult, urlparse
+
+
+class RepoUrl:
+    """
+    Class that represents repo URL.
+
+    Attributes:
+        repo (str): Name of the repository.
+        namespace (Optional[str]): Namespace of the repository, if has any.
+        username (Optional[str]): Username of the repository owner, if can be
+            specified.
+        is_fork (bool): Flag denoting if repository is a fork, if can be
+            specified (Pagure).
+        hostname (Optional[str]): Hostname of host of the repository.
+        scheme (Optional[str]): Protocol used to access repository.
+    """
+
+    def __init__(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        username: Optional[str] = None,
+        is_fork: bool = False,
+        hostname: Optional[str] = None,
+        scheme: Optional[str] = None,
+    ) -> None:
+        self.repo = repo
+        self.namespace = namespace
+        self.username = username
+        self.is_fork = is_fork
+        self.hostname = hostname
+        self.scheme = scheme
+
+    def get_instance_url(self) -> str:
+        """
+        Returns:
+            Instance URL of host of the repository.
+        """
+        scheme = self.scheme or "http"
+        return f"{scheme}://{self.hostname}"
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, RepoUrl):
+            return False
+
+        return (
+            self.repo == o.repo
+            and self.namespace == o.namespace
+            and self.username == o.username
+            and self.is_fork == o.is_fork
+            and self.hostname == o.hostname
+            and self.scheme == o.scheme
+        )
+
+    def __str__(self) -> str:
+        repo_url_str = (
+            f"RepoUrl(repo='{self.repo}', "
+            f"namespace='{self.namespace}', "
+            f"is_fork={self.is_fork}, "
+            f"hostname='{self.hostname}', "
+            f"scheme='{self.scheme}'"
+        )
+        if self.username:
+            repo_url_str += f", username='{self.username}'"
+        repo_url_str += ")"
+        return repo_url_str
+
+    def __repr__(self) -> str:
+        return str(self)
+
+    @staticmethod
+    def _prepare_url(potential_url: str) -> Optional[ParseResult]:
+        # Remove trailing '/' to ensure parsing works as expected.
+        potential_url = potential_url.rstrip("/")
+
+        # transform SSH URL
+        if "@" in potential_url:
+            split = potential_url.split("@")
+            if len(split) == 2:
+                potential_url = "https://" + split[1]
+            else:
+                # more @s ?
+                return None
+
+        # make it parsable by urlparse if it doesn't contain scheme
+        if not potential_url.startswith(
+            ("http://", "https://", "git://", "git+https://"),
+        ):
+            potential_url = "https://" + potential_url
+
+        return urlparse(potential_url)
+
+    def _set_hostname_and_scheme(self, parsed_url: ParseResult):
+        self.hostname = parsed_url.hostname
+        self.scheme = parsed_url.scheme
+
+    def _parse_username(self, parsed: ParseResult) -> bool:
+        if ":" not in parsed.netloc:
+            return True
+
+        split = parsed.netloc.split(":")
+        if len(split) > 2:
+            # in case there is port:namespace
+            return False
+
+        if not split[1]:
+            return True
+
+        if split[1] == "forks":
+            self.is_fork = True
+        elif not split[1].isnumeric():
+            # e.g. domain.com:foo or domain.com:1234,
+            # where foo is username, but 1234 is port number
+            self.username = split[1]
+
+        return True
+
+    @staticmethod
+    def _prepare_path(parsed: ParseResult) -> tuple[str, list[str]]:
+        # path starts with '/', strip it away
+        path = parsed.path.lstrip("/")
+
+        # strip trailing '.git'
+        if path.endswith(".git"):
+            path = path[:-4]
+
+        return path, path.split("/")
+
+    def _check_fork(self, splits: list[str]) -> list[str]:
+        if self.is_fork:
+            # we got pagure fork but SSH url
+            self.username = splits[0]
+            return splits[1:-1]
+
+        # path contains username/reponame
+        # or some/namespace/reponame
+        # or fork/username/some/namespace/reponame
+        self.is_fork = (
+            splits[0] in ("fork", "forks") and len(splits) >= 3
+        )  # pagure fork
+        if self.is_fork:
+            # fork/username/namespace/repo format
+            self.username = splits[1]
+            return splits[2:-1]
+
+        if self.username:
+            return [self.username] + splits[:-1]
+
+        self.username = splits[0]
+        return splits[:-1]
+
+    def _parsed_path(self, path: str, splits: list[str]) -> Optional["RepoUrl"]:
+        if len(splits) == 1:
+            self.namespace = self.username
+            self.repo = path
+
+            return self
+
+        if len(splits) < 2:
+            # invalid cases
+            return None
+
+        namespace_parts = self._check_fork(splits)
+
+        self.namespace = "/".join(namespace_parts)
+        self.repo = splits[-1]
+
+        return self
+
+    @classmethod
+    def parse(cls, potential_url: str) -> Optional["RepoUrl"]:
+        """
+        Details in `parse_git_repo` function.
+
+        Args:
+            potential_url: URL of a git repository.
+
+        Returns:
+            RepoUrl instance if can be parsed, `None` otherwise.
+        """
+        if not potential_url:
+            return None
+
+        repo = RepoUrl(None)
+        parsed_url = cls._prepare_url(potential_url)
+        if not parsed_url:
+            return None
+
+        repo._set_hostname_and_scheme(parsed_url)
+        if not repo._parse_username(parsed_url):
+            # failed parsing username
+            return None
+
+        return repo if repo._parsed_path(*cls._prepare_path(parsed_url)) else None
+
+
+def parse_git_repo(potential_url: str) -> Optional[RepoUrl]:
+    """
+    Parses given URL of a git repository.
+
+    ### Covered scenarios
+
+    1. URL in form: `www.domain.com/foo/bar`
+        - with trailing `.git`
+        - without `www.`
+        - starting with `http://`, `https://`, `git://` or `git+https://`
+    2. URL in form: `git@domain.com:foo/bar`
+        - with trailing `.git`
+        - with `ssh://` at the start or with `git+ssh` instead of `git`
+    8. Pagure format of forks, e.g.
+       `domain.com/fork/username/namespace/project`
+    9. Nested groups on GitLab or Pagure (empty namespace is supported as well)
+
+    Args:
+        potential_url: URL of a git repository.
+
+    Returns:
+        Object of RepoUrl class if can be parsed, `None` otherwise.
+    """
+    return RepoUrl.parse(potential_url)
+
+
+def get_username_from_git_url(url: str) -> Optional[str]:
+    """
+    Returns username from the git URL.
+
+    Args:
+        url: URL of the git repository.
+
+    Returns:
+        Username if can be parsed, `None` otherwise.
+    """
+    repo_url = parse_git_repo(url)
+    if repo_url:
+        return repo_url.username
+    return None
+
+
+def get_reponame_from_git_url(url: str) -> Optional[str]:
+    """
+    Returns repository name from the git URL.
+
+    Args:
+        url: URL of the git repository.
+
+    Returns:
+        Repository name if can be parsed, `None` otherwise.
+    """
+    repo_url = parse_git_repo(url)
+    if repo_url:
+        return repo_url.repo
+    return None
+
+
+def strip_dot_git(url: str) -> str:
+    """
+    Strips `.git` from the given URL of a git repository.
+
+    Args:
+        url: URL of the git repository.
+
+    Returns:
+        URL without trailing `.git`.
+    """
+    """Strip trailing .git"""
+    return url[: -len(".git")] if url.endswith(".git") else url
+
+
+
+
+
+
+
+

Functions

+
+
+def get_reponame_from_git_url(url: str) ‑> Optional[str] +
+
+

Returns repository name from the git URL.

+

Args

+
+
url
+
URL of the git repository.
+
+

Returns

+

Repository name if can be parsed, None otherwise.

+
+ +Expand source code + +
def get_reponame_from_git_url(url: str) -> Optional[str]:
+    """
+    Returns repository name from the git URL.
+
+    Args:
+        url: URL of the git repository.
+
+    Returns:
+        Repository name if can be parsed, `None` otherwise.
+    """
+    repo_url = parse_git_repo(url)
+    if repo_url:
+        return repo_url.repo
+    return None
+
+
+
+def get_username_from_git_url(url: str) ‑> Optional[str] +
+
+

Returns username from the git URL.

+

Args

+
+
url
+
URL of the git repository.
+
+

Returns

+

Username if can be parsed, None otherwise.

+
+ +Expand source code + +
def get_username_from_git_url(url: str) -> Optional[str]:
+    """
+    Returns username from the git URL.
+
+    Args:
+        url: URL of the git repository.
+
+    Returns:
+        Username if can be parsed, `None` otherwise.
+    """
+    repo_url = parse_git_repo(url)
+    if repo_url:
+        return repo_url.username
+    return None
+
+
+
+def parse_git_repo(potential_url: str) ‑> Optional[RepoUrl] +
+
+

Parses given URL of a git repository.

+

Covered scenarios

+
    +
  1. URL in form: www.domain.com/foo/bar
      +
    • with trailing .git
    • +
    • without www.
    • +
    • starting with http://, https://, git:// or git+https://
    • +
    +
  2. +
  3. URL in form: git@domain.com:foo/bar
      +
    • with trailing .git
    • +
    • with ssh:// at the start or with git+ssh instead of git
    • +
    +
  4. +
  5. Pagure format of forks, e.g. +domain.com/fork/username/namespace/project
  6. +
  7. Nested groups on GitLab or Pagure (empty namespace is supported as well)
  8. +
+

Args

+
+
potential_url
+
URL of a git repository.
+
+

Returns

+

Object of RepoUrl class if can be parsed, None otherwise.

+
+ +Expand source code + +
def parse_git_repo(potential_url: str) -> Optional[RepoUrl]:
+    """
+    Parses given URL of a git repository.
+
+    ### Covered scenarios
+
+    1. URL in form: `www.domain.com/foo/bar`
+        - with trailing `.git`
+        - without `www.`
+        - starting with `http://`, `https://`, `git://` or `git+https://`
+    2. URL in form: `git@domain.com:foo/bar`
+        - with trailing `.git`
+        - with `ssh://` at the start or with `git+ssh` instead of `git`
+    8. Pagure format of forks, e.g.
+       `domain.com/fork/username/namespace/project`
+    9. Nested groups on GitLab or Pagure (empty namespace is supported as well)
+
+    Args:
+        potential_url: URL of a git repository.
+
+    Returns:
+        Object of RepoUrl class if can be parsed, `None` otherwise.
+    """
+    return RepoUrl.parse(potential_url)
+
+
+
+def strip_dot_git(url: str) ‑> str +
+
+

Strips .git from the given URL of a git repository.

+

Args

+
+
url
+
URL of the git repository.
+
+

Returns

+

URL without trailing .git.

+
+ +Expand source code + +
def strip_dot_git(url: str) -> str:
+    """
+    Strips `.git` from the given URL of a git repository.
+
+    Args:
+        url: URL of the git repository.
+
+    Returns:
+        URL without trailing `.git`.
+    """
+    """Strip trailing .git"""
+    return url[: -len(".git")] if url.endswith(".git") else url
+
+
+
+
+
+

Classes

+
+
+class RepoUrl +(repo: str, namespace: Optional[str] = None, username: Optional[str] = None, is_fork: bool = False, hostname: Optional[str] = None, scheme: Optional[str] = None) +
+
+

Class that represents repo URL.

+

Attributes

+
+
repo : str
+
Name of the repository.
+
namespace : Optional[str]
+
Namespace of the repository, if has any.
+
username : Optional[str]
+
Username of the repository owner, if can be +specified.
+
is_fork : bool
+
Flag denoting if repository is a fork, if can be +specified (Pagure).
+
hostname : Optional[str]
+
Hostname of host of the repository.
+
scheme : Optional[str]
+
Protocol used to access repository.
+
+
+ +Expand source code + +
class RepoUrl:
+    """
+    Class that represents repo URL.
+
+    Attributes:
+        repo (str): Name of the repository.
+        namespace (Optional[str]): Namespace of the repository, if has any.
+        username (Optional[str]): Username of the repository owner, if can be
+            specified.
+        is_fork (bool): Flag denoting if repository is a fork, if can be
+            specified (Pagure).
+        hostname (Optional[str]): Hostname of host of the repository.
+        scheme (Optional[str]): Protocol used to access repository.
+    """
+
+    def __init__(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        username: Optional[str] = None,
+        is_fork: bool = False,
+        hostname: Optional[str] = None,
+        scheme: Optional[str] = None,
+    ) -> None:
+        self.repo = repo
+        self.namespace = namespace
+        self.username = username
+        self.is_fork = is_fork
+        self.hostname = hostname
+        self.scheme = scheme
+
+    def get_instance_url(self) -> str:
+        """
+        Returns:
+            Instance URL of host of the repository.
+        """
+        scheme = self.scheme or "http"
+        return f"{scheme}://{self.hostname}"
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, RepoUrl):
+            return False
+
+        return (
+            self.repo == o.repo
+            and self.namespace == o.namespace
+            and self.username == o.username
+            and self.is_fork == o.is_fork
+            and self.hostname == o.hostname
+            and self.scheme == o.scheme
+        )
+
+    def __str__(self) -> str:
+        repo_url_str = (
+            f"RepoUrl(repo='{self.repo}', "
+            f"namespace='{self.namespace}', "
+            f"is_fork={self.is_fork}, "
+            f"hostname='{self.hostname}', "
+            f"scheme='{self.scheme}'"
+        )
+        if self.username:
+            repo_url_str += f", username='{self.username}'"
+        repo_url_str += ")"
+        return repo_url_str
+
+    def __repr__(self) -> str:
+        return str(self)
+
+    @staticmethod
+    def _prepare_url(potential_url: str) -> Optional[ParseResult]:
+        # Remove trailing '/' to ensure parsing works as expected.
+        potential_url = potential_url.rstrip("/")
+
+        # transform SSH URL
+        if "@" in potential_url:
+            split = potential_url.split("@")
+            if len(split) == 2:
+                potential_url = "https://" + split[1]
+            else:
+                # more @s ?
+                return None
+
+        # make it parsable by urlparse if it doesn't contain scheme
+        if not potential_url.startswith(
+            ("http://", "https://", "git://", "git+https://"),
+        ):
+            potential_url = "https://" + potential_url
+
+        return urlparse(potential_url)
+
+    def _set_hostname_and_scheme(self, parsed_url: ParseResult):
+        self.hostname = parsed_url.hostname
+        self.scheme = parsed_url.scheme
+
+    def _parse_username(self, parsed: ParseResult) -> bool:
+        if ":" not in parsed.netloc:
+            return True
+
+        split = parsed.netloc.split(":")
+        if len(split) > 2:
+            # in case there is port:namespace
+            return False
+
+        if not split[1]:
+            return True
+
+        if split[1] == "forks":
+            self.is_fork = True
+        elif not split[1].isnumeric():
+            # e.g. domain.com:foo or domain.com:1234,
+            # where foo is username, but 1234 is port number
+            self.username = split[1]
+
+        return True
+
+    @staticmethod
+    def _prepare_path(parsed: ParseResult) -> tuple[str, list[str]]:
+        # path starts with '/', strip it away
+        path = parsed.path.lstrip("/")
+
+        # strip trailing '.git'
+        if path.endswith(".git"):
+            path = path[:-4]
+
+        return path, path.split("/")
+
+    def _check_fork(self, splits: list[str]) -> list[str]:
+        if self.is_fork:
+            # we got pagure fork but SSH url
+            self.username = splits[0]
+            return splits[1:-1]
+
+        # path contains username/reponame
+        # or some/namespace/reponame
+        # or fork/username/some/namespace/reponame
+        self.is_fork = (
+            splits[0] in ("fork", "forks") and len(splits) >= 3
+        )  # pagure fork
+        if self.is_fork:
+            # fork/username/namespace/repo format
+            self.username = splits[1]
+            return splits[2:-1]
+
+        if self.username:
+            return [self.username] + splits[:-1]
+
+        self.username = splits[0]
+        return splits[:-1]
+
+    def _parsed_path(self, path: str, splits: list[str]) -> Optional["RepoUrl"]:
+        if len(splits) == 1:
+            self.namespace = self.username
+            self.repo = path
+
+            return self
+
+        if len(splits) < 2:
+            # invalid cases
+            return None
+
+        namespace_parts = self._check_fork(splits)
+
+        self.namespace = "/".join(namespace_parts)
+        self.repo = splits[-1]
+
+        return self
+
+    @classmethod
+    def parse(cls, potential_url: str) -> Optional["RepoUrl"]:
+        """
+        Details in `parse_git_repo` function.
+
+        Args:
+            potential_url: URL of a git repository.
+
+        Returns:
+            RepoUrl instance if can be parsed, `None` otherwise.
+        """
+        if not potential_url:
+            return None
+
+        repo = RepoUrl(None)
+        parsed_url = cls._prepare_url(potential_url)
+        if not parsed_url:
+            return None
+
+        repo._set_hostname_and_scheme(parsed_url)
+        if not repo._parse_username(parsed_url):
+            # failed parsing username
+            return None
+
+        return repo if repo._parsed_path(*cls._prepare_path(parsed_url)) else None
+
+

Static methods

+
+
+def parse(potential_url: str) ‑> Optional[RepoUrl] +
+
+

Details in parse_git_repo() function.

+

Args

+
+
potential_url
+
URL of a git repository.
+
+

Returns

+

RepoUrl instance if can be parsed, None otherwise.

+
+ +Expand source code + +
@classmethod
+def parse(cls, potential_url: str) -> Optional["RepoUrl"]:
+    """
+    Details in `parse_git_repo` function.
+
+    Args:
+        potential_url: URL of a git repository.
+
+    Returns:
+        RepoUrl instance if can be parsed, `None` otherwise.
+    """
+    if not potential_url:
+        return None
+
+    repo = RepoUrl(None)
+    parsed_url = cls._prepare_url(potential_url)
+    if not parsed_url:
+        return None
+
+    repo._set_hostname_and_scheme(parsed_url)
+    if not repo._parse_username(parsed_url):
+        # failed parsing username
+        return None
+
+    return repo if repo._parsed_path(*cls._prepare_path(parsed_url)) else None
+
+
+
+

Methods

+
+
+def get_instance_url(self) ‑> str +
+
+

Returns

+

Instance URL of host of the repository.

+
+ +Expand source code + +
def get_instance_url(self) -> str:
+    """
+    Returns:
+        Instance URL of host of the repository.
+    """
+    scheme = self.scheme or "http"
+    return f"{scheme}://{self.hostname}"
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/read_only.html b/docs/read_only.html new file mode 100644 index 00000000..6d4e88d1 --- /dev/null +++ b/docs/read_only.html @@ -0,0 +1,925 @@ + + + + + + +ogr.read_only API documentation + + + + + + + + + + + +
+
+
+

Module ogr.read_only

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+import functools
+import logging
+from typing import Any, Callable, Optional
+
+from ogr.abstract import (
+    CommitComment,
+    CommitFlag,
+    CommitStatus,
+    GitProject,
+    IssueComment,
+    PRComment,
+    PRStatus,
+    PullRequest,
+)
+from ogr.constant import DEFAULT_RO_PREFIX_STRING
+
+
+def log_output(
+    text: str,
+    default_prefix: str = DEFAULT_RO_PREFIX_STRING,
+    namespace: str = __name__,
+) -> None:
+    """
+    Logs output.
+
+    Args:
+        text: Text message to be logged.
+        default_prefix: Prefix of the log message.
+
+            Defaults to `DEFAULT_RO_PREFIX_STRING`.
+        namespace: Namespace where the message comes from.
+
+            Defaults to `__name__`.
+    """
+    logger = logging.getLogger(namespace)
+    logger.warning(f"{default_prefix} {text}")
+
+
+def if_readonly(
+    *,
+    return_value: Optional[Any] = None,
+    return_function: Optional[Callable] = None,
+    log_message: str = "",
+) -> Any:
+    """
+    Decorator to log function and ovewrite return value of object methods.
+    Ignore function name as first parameter and ignore every other parameters.
+
+    Args:
+        return_value: Returned value if given, `return_function`
+            has higher priority if set.
+
+            Defaults to `None`.
+        return_function: Returned function with applies
+            arguments and original caller.
+
+            Defaults to `None`.
+        log_message: String to be put to logger output.
+
+            Defaults to `""`.
+
+    Returns:
+        Any value that is expected to be returned from the function call or the
+        specificied return value.
+    """
+
+    def decorator_readonly(func):
+        @functools.wraps(func)
+        def readonly_func(self, *args, **kwargs):
+            if not self.read_only:
+                return func(self, *args, **kwargs)
+
+            args_str = str(args)[1:-1]
+            kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
+            # add , in case there are also args, what has to be separated
+            if args and kwargs:
+                kwargs_str = ", " + kwargs_str
+            log_output(
+                f"{log_message} {self.__class__.__name__}."
+                f"{func.__name__}({args_str}{kwargs_str})",
+            )
+            if return_function:
+                return return_function(self, *args, **kwargs)
+
+            return return_value
+
+        return readonly_func
+
+    return decorator_readonly
+
+
+class PullRequestReadOnly(PullRequest):
+    def __init__(
+        self,
+        title: str,
+        description: str,
+        target_branch: str,
+        source_branch: str,
+        id: int,
+        status: PRStatus,
+        url: str,
+        author: str,
+        created: datetime.datetime,
+    ) -> None:
+        self._title = title
+        self._description = description
+        self._target_branch = target_branch
+        self._source_branch = source_branch
+        self._id = id
+        self._status = PRStatus.open
+        self._url = url
+        self._author = author
+        self._created = created
+
+    @property
+    def title(self) -> str:
+        return self._title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._title = new_title
+
+    @property
+    def id(self) -> int:
+        return self._id
+
+    @property
+    def status(self) -> PRStatus:
+        return self._status
+
+    @property
+    def url(self) -> str:
+        return self._url
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._description = new_description
+
+    @property
+    def author(self) -> str:
+        return self._author
+
+    @property
+    def source_branch(self) -> str:
+        return self._source_branch
+
+    @property
+    def target_branch(self) -> str:
+        return self._target_branch
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._created
+
+
+class GitProjectReadOnly:
+    id = 1
+    author = "ReadOnlyAuthor"
+    url = "url://ReadOnlyURL"
+
+    @classmethod
+    def create_pr(
+        cls,
+        original_object: Any,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        return PullRequestReadOnly(
+            title=title,
+            description=body,
+            target_branch=target_branch,
+            source_branch=source_branch,
+            id=cls.id,
+            status=PRStatus.open,
+            url=cls.url,
+            author=cls.author,
+            created=datetime.datetime.now(),
+        )
+
+    @classmethod
+    def pr_comment(
+        cls,
+        original_object: Any,
+        pr_id: int,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        pull_request = original_object.get_pr(pr_id)
+        log_output(pull_request)
+        return PRComment(
+            parent=pull_request,
+            body=body,
+            author=cls.author,
+            created=datetime.datetime.now(),
+            edited=datetime.datetime.now(),
+        )
+
+    @classmethod
+    def pr_close(cls, original_object: Any, pr_id: int) -> "PullRequest":
+        pull_request = original_object.get_pr(pr_id)
+        pull_request._status = PRStatus.closed
+        return pull_request
+
+    @classmethod
+    def pr_merge(cls, original_object: Any, pr_id: int) -> "PullRequest":
+        pull_request = original_object.get_pr(pr_id)
+        pull_request._status = PRStatus.merged
+        return pull_request
+
+    @classmethod
+    def issue_comment(
+        cls,
+        original_object: Any,
+        issue_id: int,
+        body: str,
+    ) -> "IssueComment":
+        issue = original_object.get_issue(issue_id)
+        log_output(issue)
+        return IssueComment(
+            parent=issue,
+            body=body,
+            author=cls.author,
+            created=datetime.datetime.now(),
+            edited=datetime.datetime.now(),
+        )
+
+    @classmethod
+    def fork_create(
+        cls,
+        original_object: Any,
+        namespace: Optional[str] = None,
+    ) -> "GitProject":
+        return original_object
+
+    @classmethod
+    def commit_comment(
+        cls,
+        original_object: Any,
+        commit: str,
+        body: str,
+    ) -> "CommitComment":
+        return CommitComment(sha=commit, body=body, author=cls.author)
+
+    @classmethod
+    def set_commit_status(
+        cls,
+        original_object: Any,
+        commit: str,
+        state: CommitStatus,
+        context: str,
+    ) -> "CommitFlag":
+        return CommitFlag(commit=commit, state=state, context=context)
+
+
+
+
+
+
+
+

Functions

+
+
+def if_readonly(*, return_value: Optional[Any] = None, return_function: Optional[Callable] = None, log_message: str = '') ‑> Any +
+
+

Decorator to log function and ovewrite return value of object methods. +Ignore function name as first parameter and ignore every other parameters.

+

Args

+
+
return_value
+
+

Returned value if given, return_function +has higher priority if set.

+

Defaults to None.

+
+
return_function
+
+

Returned function with applies +arguments and original caller.

+

Defaults to None.

+
+
log_message
+
+

String to be put to logger output.

+

Defaults to "".

+
+
+

Returns

+

Any value that is expected to be returned from the function call or the +specificied return value.

+
+ +Expand source code + +
def if_readonly(
+    *,
+    return_value: Optional[Any] = None,
+    return_function: Optional[Callable] = None,
+    log_message: str = "",
+) -> Any:
+    """
+    Decorator to log function and ovewrite return value of object methods.
+    Ignore function name as first parameter and ignore every other parameters.
+
+    Args:
+        return_value: Returned value if given, `return_function`
+            has higher priority if set.
+
+            Defaults to `None`.
+        return_function: Returned function with applies
+            arguments and original caller.
+
+            Defaults to `None`.
+        log_message: String to be put to logger output.
+
+            Defaults to `""`.
+
+    Returns:
+        Any value that is expected to be returned from the function call or the
+        specificied return value.
+    """
+
+    def decorator_readonly(func):
+        @functools.wraps(func)
+        def readonly_func(self, *args, **kwargs):
+            if not self.read_only:
+                return func(self, *args, **kwargs)
+
+            args_str = str(args)[1:-1]
+            kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
+            # add , in case there are also args, what has to be separated
+            if args and kwargs:
+                kwargs_str = ", " + kwargs_str
+            log_output(
+                f"{log_message} {self.__class__.__name__}."
+                f"{func.__name__}({args_str}{kwargs_str})",
+            )
+            if return_function:
+                return return_function(self, *args, **kwargs)
+
+            return return_value
+
+        return readonly_func
+
+    return decorator_readonly
+
+
+
+def log_output(text: str, default_prefix: str = 'READ ONLY: ', namespace: str = 'ogr.read_only') ‑> None +
+
+

Logs output.

+

Args

+
+
text
+
Text message to be logged.
+
default_prefix
+
+

Prefix of the log message.

+

Defaults to DEFAULT_RO_PREFIX_STRING.

+
+
namespace
+
+

Namespace where the message comes from.

+

Defaults to __name__.

+
+
+
+ +Expand source code + +
def log_output(
+    text: str,
+    default_prefix: str = DEFAULT_RO_PREFIX_STRING,
+    namespace: str = __name__,
+) -> None:
+    """
+    Logs output.
+
+    Args:
+        text: Text message to be logged.
+        default_prefix: Prefix of the log message.
+
+            Defaults to `DEFAULT_RO_PREFIX_STRING`.
+        namespace: Namespace where the message comes from.
+
+            Defaults to `__name__`.
+    """
+    logger = logging.getLogger(namespace)
+    logger.warning(f"{default_prefix} {text}")
+
+
+
+
+
+

Classes

+
+
+class GitProjectReadOnly +
+
+
+
+ +Expand source code + +
class GitProjectReadOnly:
+    id = 1
+    author = "ReadOnlyAuthor"
+    url = "url://ReadOnlyURL"
+
+    @classmethod
+    def create_pr(
+        cls,
+        original_object: Any,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        return PullRequestReadOnly(
+            title=title,
+            description=body,
+            target_branch=target_branch,
+            source_branch=source_branch,
+            id=cls.id,
+            status=PRStatus.open,
+            url=cls.url,
+            author=cls.author,
+            created=datetime.datetime.now(),
+        )
+
+    @classmethod
+    def pr_comment(
+        cls,
+        original_object: Any,
+        pr_id: int,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        pull_request = original_object.get_pr(pr_id)
+        log_output(pull_request)
+        return PRComment(
+            parent=pull_request,
+            body=body,
+            author=cls.author,
+            created=datetime.datetime.now(),
+            edited=datetime.datetime.now(),
+        )
+
+    @classmethod
+    def pr_close(cls, original_object: Any, pr_id: int) -> "PullRequest":
+        pull_request = original_object.get_pr(pr_id)
+        pull_request._status = PRStatus.closed
+        return pull_request
+
+    @classmethod
+    def pr_merge(cls, original_object: Any, pr_id: int) -> "PullRequest":
+        pull_request = original_object.get_pr(pr_id)
+        pull_request._status = PRStatus.merged
+        return pull_request
+
+    @classmethod
+    def issue_comment(
+        cls,
+        original_object: Any,
+        issue_id: int,
+        body: str,
+    ) -> "IssueComment":
+        issue = original_object.get_issue(issue_id)
+        log_output(issue)
+        return IssueComment(
+            parent=issue,
+            body=body,
+            author=cls.author,
+            created=datetime.datetime.now(),
+            edited=datetime.datetime.now(),
+        )
+
+    @classmethod
+    def fork_create(
+        cls,
+        original_object: Any,
+        namespace: Optional[str] = None,
+    ) -> "GitProject":
+        return original_object
+
+    @classmethod
+    def commit_comment(
+        cls,
+        original_object: Any,
+        commit: str,
+        body: str,
+    ) -> "CommitComment":
+        return CommitComment(sha=commit, body=body, author=cls.author)
+
+    @classmethod
+    def set_commit_status(
+        cls,
+        original_object: Any,
+        commit: str,
+        state: CommitStatus,
+        context: str,
+    ) -> "CommitFlag":
+        return CommitFlag(commit=commit, state=state, context=context)
+
+

Class variables

+
+
var author
+
+
+
+
var id
+
+
+
+
var url
+
+
+
+
+

Static methods

+
+
+def commit_comment(original_object: Any, commit: str, body: str) ‑> CommitComment +
+
+
+
+ +Expand source code + +
@classmethod
+def commit_comment(
+    cls,
+    original_object: Any,
+    commit: str,
+    body: str,
+) -> "CommitComment":
+    return CommitComment(sha=commit, body=body, author=cls.author)
+
+
+
+def create_pr(original_object: Any, title: str, body: str, target_branch: str, source_branch: str, fork_username: Optional[str] = None) ‑> PullRequest +
+
+
+
+ +Expand source code + +
@classmethod
+def create_pr(
+    cls,
+    original_object: Any,
+    title: str,
+    body: str,
+    target_branch: str,
+    source_branch: str,
+    fork_username: Optional[str] = None,
+) -> "PullRequest":
+    return PullRequestReadOnly(
+        title=title,
+        description=body,
+        target_branch=target_branch,
+        source_branch=source_branch,
+        id=cls.id,
+        status=PRStatus.open,
+        url=cls.url,
+        author=cls.author,
+        created=datetime.datetime.now(),
+    )
+
+
+
+def fork_create(original_object: Any, namespace: Optional[str] = None) ‑> GitProject +
+
+
+
+ +Expand source code + +
@classmethod
+def fork_create(
+    cls,
+    original_object: Any,
+    namespace: Optional[str] = None,
+) -> "GitProject":
+    return original_object
+
+
+
+def issue_comment(original_object: Any, issue_id: int, body: str) ‑> IssueComment +
+
+
+
+ +Expand source code + +
@classmethod
+def issue_comment(
+    cls,
+    original_object: Any,
+    issue_id: int,
+    body: str,
+) -> "IssueComment":
+    issue = original_object.get_issue(issue_id)
+    log_output(issue)
+    return IssueComment(
+        parent=issue,
+        body=body,
+        author=cls.author,
+        created=datetime.datetime.now(),
+        edited=datetime.datetime.now(),
+    )
+
+
+
+def pr_close(original_object: Any, pr_id: int) ‑> PullRequest +
+
+
+
+ +Expand source code + +
@classmethod
+def pr_close(cls, original_object: Any, pr_id: int) -> "PullRequest":
+    pull_request = original_object.get_pr(pr_id)
+    pull_request._status = PRStatus.closed
+    return pull_request
+
+
+
+def pr_comment(original_object: Any, pr_id: int, body: str, commit: Optional[str] = None, filename: Optional[str] = None, row: Optional[int] = None) ‑> PRComment +
+
+
+
+ +Expand source code + +
@classmethod
+def pr_comment(
+    cls,
+    original_object: Any,
+    pr_id: int,
+    body: str,
+    commit: Optional[str] = None,
+    filename: Optional[str] = None,
+    row: Optional[int] = None,
+) -> "PRComment":
+    pull_request = original_object.get_pr(pr_id)
+    log_output(pull_request)
+    return PRComment(
+        parent=pull_request,
+        body=body,
+        author=cls.author,
+        created=datetime.datetime.now(),
+        edited=datetime.datetime.now(),
+    )
+
+
+
+def pr_merge(original_object: Any, pr_id: int) ‑> PullRequest +
+
+
+
+ +Expand source code + +
@classmethod
+def pr_merge(cls, original_object: Any, pr_id: int) -> "PullRequest":
+    pull_request = original_object.get_pr(pr_id)
+    pull_request._status = PRStatus.merged
+    return pull_request
+
+
+
+def set_commit_status(original_object: Any, commit: str, state: CommitStatus, context: str) ‑> CommitFlag +
+
+
+
+ +Expand source code + +
@classmethod
+def set_commit_status(
+    cls,
+    original_object: Any,
+    commit: str,
+    state: CommitStatus,
+    context: str,
+) -> "CommitFlag":
+    return CommitFlag(commit=commit, state=state, context=context)
+
+
+
+
+
+class PullRequestReadOnly +(title: str, description: str, target_branch: str, source_branch: str, id: int, status: PRStatus, url: str, author: str, created: datetime.datetime) +
+
+

Attributes

+
+
project : GitProject
+
Project of the pull request.
+
+
+ +Expand source code + +
class PullRequestReadOnly(PullRequest):
+    def __init__(
+        self,
+        title: str,
+        description: str,
+        target_branch: str,
+        source_branch: str,
+        id: int,
+        status: PRStatus,
+        url: str,
+        author: str,
+        created: datetime.datetime,
+    ) -> None:
+        self._title = title
+        self._description = description
+        self._target_branch = target_branch
+        self._source_branch = source_branch
+        self._id = id
+        self._status = PRStatus.open
+        self._url = url
+        self._author = author
+        self._created = created
+
+    @property
+    def title(self) -> str:
+        return self._title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._title = new_title
+
+    @property
+    def id(self) -> int:
+        return self._id
+
+    @property
+    def status(self) -> PRStatus:
+        return self._status
+
+    @property
+    def url(self) -> str:
+        return self._url
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._description = new_description
+
+    @property
+    def author(self) -> str:
+        return self._author
+
+    @property
+    def source_branch(self) -> str:
+        return self._source_branch
+
+    @property
+    def target_branch(self) -> str:
+        return self._target_branch
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._created
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/base.html b/docs/services/base.html new file mode 100644 index 00000000..76f289ac --- /dev/null +++ b/docs/services/base.html @@ -0,0 +1,663 @@ + + + + + + +ogr.services.base API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.base

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from typing import Any, Optional
+from urllib.request import urlopen
+
+from ogr.abstract import (
+    CommitFlag,
+    CommitStatus,
+    GitProject,
+    GitService,
+    GitUser,
+    Issue,
+    IssueComment,
+    PullRequest,
+    Release,
+)
+from ogr.exceptions import OgrException
+from ogr.parsing import parse_git_repo
+from ogr.utils import filter_comments, search_in_comments
+
+try:
+    from functools import cached_property
+except ImportError:
+    from functools import lru_cache
+
+    def cached_property(func):  # type: ignore
+        return property(lru_cache()(func))
+
+
+class BaseGitService(GitService):
+    @cached_property
+    def hostname(self) -> Optional[str]:
+        parsed_url = parse_git_repo(potential_url=self.instance_url)
+        return parsed_url.hostname if parsed_url else None
+
+    def get_project_from_url(self, url: str) -> "GitProject":
+        repo_url = parse_git_repo(potential_url=url)
+        if not repo_url:
+            raise OgrException(f"Cannot parse project url: '{url}'")
+        return self.get_project(repo=repo_url.repo, namespace=repo_url.namespace)
+
+
+class BaseGitProject(GitProject):
+    @property
+    def full_repo_name(self) -> str:
+        return f"{self.namespace}/{self.repo}"
+
+
+class BasePullRequest(PullRequest):
+    @property
+    def target_branch_head_commit(self) -> str:
+        return self.target_project.get_sha_from_branch(self.target_branch)
+
+    def get_comments(
+        self,
+        filter_regex: Optional[str] = None,
+        reverse: bool = False,
+        author: Optional[str] = None,
+    ):
+        all_comments = self._get_all_comments()
+        return filter_comments(all_comments, filter_regex, reverse, author)
+
+    def search(
+        self,
+        filter_regex: str,
+        reverse: bool = False,
+        description: bool = True,
+    ):
+        all_comments: list[Any] = self.get_comments(reverse=reverse)
+        if description:
+            description_content = self.description
+            if reverse:
+                all_comments.append(description_content)
+            else:
+                all_comments.insert(0, description_content)
+
+        return search_in_comments(comments=all_comments, filter_regex=filter_regex)
+
+    def get_statuses(self) -> list[CommitFlag]:
+        commit = self.get_all_commits()[-1]
+        return self.target_project.get_commit_statuses(commit)
+
+
+class BaseGitUser(GitUser):
+    pass
+
+
+class BaseIssue(Issue):
+    def get_comments(
+        self,
+        filter_regex: Optional[str] = None,
+        reverse: bool = False,
+        author: Optional[str] = None,
+    ) -> list[IssueComment]:
+        all_comments: list[IssueComment] = self._get_all_comments()
+        return filter_comments(all_comments, filter_regex, reverse, author)
+
+    def can_close(self, username: str) -> bool:
+        return username == self.author or username in self.project.who_can_close_issue()
+
+
+class BaseCommitFlag(CommitFlag):
+    @classmethod
+    def _state_from_str(cls, state: str) -> CommitStatus:
+        if state not in cls._states:
+            raise ValueError("Invalid state given")
+        return cls._states[state]
+
+    @classmethod
+    def _validate_state(cls, state: CommitStatus) -> CommitStatus:
+        if state not in cls._states.values():
+            raise ValueError("Invalid state given")
+
+        return state
+
+
+class BaseRelease(Release):
+    def save_archive(self, filename: str) -> None:
+        response = urlopen(self.tarball_url)
+        data = response.read()
+
+        with open(filename, "wb") as file:
+            file.write(data)
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class BaseCommitFlag +(raw_commit_flag: Optional[Any] = None, project: Optional[ForwardRef('GitProject')] = None, commit: Optional[str] = None, state: Optional[CommitStatus] = None, context: Optional[str] = None, comment: Optional[str] = None, uid: Optional[str] = None, url: Optional[str] = None) +
+
+
+
+ +Expand source code + +
class BaseCommitFlag(CommitFlag):
+    @classmethod
+    def _state_from_str(cls, state: str) -> CommitStatus:
+        if state not in cls._states:
+            raise ValueError("Invalid state given")
+        return cls._states[state]
+
+    @classmethod
+    def _validate_state(cls, state: CommitStatus) -> CommitStatus:
+        if state not in cls._states.values():
+            raise ValueError("Invalid state given")
+
+        return state
+
+

Ancestors

+ +

Subclasses

+ +

Inherited members

+ +
+
+class BaseGitProject +(repo: str, service: GitService, namespace: str) +
+
+

Args

+
+
repo
+
Name of the project.
+
service
+
GitService instance.
+
namespace
+
+

Namespace of the project.

+
    +
  • GitHub: username or org name.
  • +
  • GitLab: username or org name.
  • +
  • Pagure: namespace (e.g. "rpms").
  • +
+

In case of forks: "fork/{username}/{namespace}".

+
+
+
+ +Expand source code + +
class BaseGitProject(GitProject):
+    @property
+    def full_repo_name(self) -> str:
+        return f"{self.namespace}/{self.repo}"
+
+

Ancestors

+ +

Subclasses

+ +

Inherited members

+ +
+
+class BaseGitService +(**_: Any) +
+
+

Attributes

+
+
instance_url : str
+
URL of the git forge instance.
+
+
+ +Expand source code + +
class BaseGitService(GitService):
+    @cached_property
+    def hostname(self) -> Optional[str]:
+        parsed_url = parse_git_repo(potential_url=self.instance_url)
+        return parsed_url.hostname if parsed_url else None
+
+    def get_project_from_url(self, url: str) -> "GitProject":
+        repo_url = parse_git_repo(potential_url=url)
+        if not repo_url:
+            raise OgrException(f"Cannot parse project url: '{url}'")
+        return self.get_project(repo=repo_url.repo, namespace=repo_url.namespace)
+
+

Ancestors

+ +

Subclasses

+ +

Inherited members

+ +
+
+class BaseGitUser +(service: GitService) +
+
+

Represents currently authenticated user through service.

+
+ +Expand source code + +
class BaseGitUser(GitUser):
+    pass
+
+

Ancestors

+ +

Subclasses

+ +

Inherited members

+ +
+
+class BaseIssue +(raw_issue: Any, project: GitProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the issue.
+
+
+ +Expand source code + +
class BaseIssue(Issue):
+    def get_comments(
+        self,
+        filter_regex: Optional[str] = None,
+        reverse: bool = False,
+        author: Optional[str] = None,
+    ) -> list[IssueComment]:
+        all_comments: list[IssueComment] = self._get_all_comments()
+        return filter_comments(all_comments, filter_regex, reverse, author)
+
+    def can_close(self, username: str) -> bool:
+        return username == self.author or username in self.project.who_can_close_issue()
+
+

Ancestors

+ +

Subclasses

+ +

Inherited members

+ +
+
+class BasePullRequest +(raw_pr: Any, project: GitProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the pull request.
+
+
+ +Expand source code + +
class BasePullRequest(PullRequest):
+    @property
+    def target_branch_head_commit(self) -> str:
+        return self.target_project.get_sha_from_branch(self.target_branch)
+
+    def get_comments(
+        self,
+        filter_regex: Optional[str] = None,
+        reverse: bool = False,
+        author: Optional[str] = None,
+    ):
+        all_comments = self._get_all_comments()
+        return filter_comments(all_comments, filter_regex, reverse, author)
+
+    def search(
+        self,
+        filter_regex: str,
+        reverse: bool = False,
+        description: bool = True,
+    ):
+        all_comments: list[Any] = self.get_comments(reverse=reverse)
+        if description:
+            description_content = self.description
+            if reverse:
+                all_comments.append(description_content)
+            else:
+                all_comments.insert(0, description_content)
+
+        return search_in_comments(comments=all_comments, filter_regex=filter_regex)
+
+    def get_statuses(self) -> list[CommitFlag]:
+        commit = self.get_all_commits()[-1]
+        return self.target_project.get_commit_statuses(commit)
+
+

Ancestors

+ +

Subclasses

+ +

Inherited members

+ +
+
+class BaseRelease +(raw_release: Any, project: GitProject) +
+
+

Object that represents release.

+

Attributes

+
+
project : GitProject
+
Project on which the release is created.
+
+
+ +Expand source code + +
class BaseRelease(Release):
+    def save_archive(self, filename: str) -> None:
+        response = urlopen(self.tarball_url)
+        data = response.read()
+
+        with open(filename, "wb") as file:
+            file.write(data)
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/auth_providers/abstract.html b/docs/services/github/auth_providers/abstract.html new file mode 100644 index 00000000..bde445c9 --- /dev/null +++ b/docs/services/github/auth_providers/abstract.html @@ -0,0 +1,252 @@ + + + + + + +ogr.services.github.auth_providers.abstract API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.auth_providers.abstract

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from typing import Optional
+
+import github
+
+
+class GithubAuthentication:
+    """
+    Represents a token manager for authentication via GitHub App.
+    """
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        """
+        Get a GitHub token for requested repository.
+
+        Args:
+            namespace: Namespace of the repository.
+            repo: Name of the repository.
+
+        Returns:
+            A token that can be used in PyGithub instance for authentication.
+        """
+        raise NotImplementedError()
+
+    @property
+    def pygithub_instance(self) -> "github.Github":
+        """
+        Returns:
+            Generic PyGithub instance. Used for `GitUser` for example.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def try_create(**kwargs) -> Optional["GithubAuthentication"]:
+        """
+        Tries to construct authentication object from provided keyword arguments.
+
+        Returns:
+            `GithubAuthentication` object or `None` if the creation was not
+            successful.
+        """
+        raise NotImplementedError()
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubAuthentication +
+
+

Represents a token manager for authentication via GitHub App.

+
+ +Expand source code + +
class GithubAuthentication:
+    """
+    Represents a token manager for authentication via GitHub App.
+    """
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        """
+        Get a GitHub token for requested repository.
+
+        Args:
+            namespace: Namespace of the repository.
+            repo: Name of the repository.
+
+        Returns:
+            A token that can be used in PyGithub instance for authentication.
+        """
+        raise NotImplementedError()
+
+    @property
+    def pygithub_instance(self) -> "github.Github":
+        """
+        Returns:
+            Generic PyGithub instance. Used for `GitUser` for example.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def try_create(**kwargs) -> Optional["GithubAuthentication"]:
+        """
+        Tries to construct authentication object from provided keyword arguments.
+
+        Returns:
+            `GithubAuthentication` object or `None` if the creation was not
+            successful.
+        """
+        raise NotImplementedError()
+
+

Subclasses

+ +

Static methods

+
+
+def try_create(**kwargs) ‑> Optional[GithubAuthentication] +
+
+

Tries to construct authentication object from provided keyword arguments.

+

Returns

+

GithubAuthentication object or None if the creation was not +successful.

+
+ +Expand source code + +
@staticmethod
+def try_create(**kwargs) -> Optional["GithubAuthentication"]:
+    """
+    Tries to construct authentication object from provided keyword arguments.
+
+    Returns:
+        `GithubAuthentication` object or `None` if the creation was not
+        successful.
+    """
+    raise NotImplementedError()
+
+
+
+

Instance variables

+
+
var pygithub_instance : github.MainClass.Github
+
+

Returns

+

Generic PyGithub instance. Used for GitUser for example.

+
+ +Expand source code + +
@property
+def pygithub_instance(self) -> "github.Github":
+    """
+    Returns:
+        Generic PyGithub instance. Used for `GitUser` for example.
+    """
+    raise NotImplementedError()
+
+
+
+

Methods

+
+
+def get_token(self, namespace: str, repo: str) ‑> str +
+
+

Get a GitHub token for requested repository.

+

Args

+
+
namespace
+
Namespace of the repository.
+
repo
+
Name of the repository.
+
+

Returns

+

A token that can be used in PyGithub instance for authentication.

+
+ +Expand source code + +
def get_token(self, namespace: str, repo: str) -> str:
+    """
+    Get a GitHub token for requested repository.
+
+    Args:
+        namespace: Namespace of the repository.
+        repo: Name of the repository.
+
+    Returns:
+        A token that can be used in PyGithub instance for authentication.
+    """
+    raise NotImplementedError()
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/auth_providers/github_app.html b/docs/services/github/auth_providers/github_app.html new file mode 100644 index 00000000..5b8a3850 --- /dev/null +++ b/docs/services/github/auth_providers/github_app.html @@ -0,0 +1,367 @@ + + + + + + +ogr.services.github.auth_providers.github_app API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.auth_providers.github_app

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from pathlib import Path
+from typing import Optional
+
+import github
+
+from ogr.exceptions import OgrException
+from ogr.services.github.auth_providers.abstract import GithubAuthentication
+
+
+class GithubApp(GithubAuthentication):
+    def __init__(self, id: str, private_key: str, private_key_path: str) -> None:
+        self.id = id
+        self._private_key = private_key
+        self._private_key_path = private_key_path
+
+        self._github: github.Github = None
+        self._integration: github.GithubIntegration = None
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, GithubApp):
+            return False
+
+        return (
+            self.id == o.id  # type: ignore
+            and self._private_key == o._private_key  # type: ignore
+            and self._private_key_path == o._private_key_path  # type: ignore
+        )
+
+    def __str__(self) -> str:
+        censored_id = f"id='{self.id[:1]}***{self.id[-1:]}'"
+        censored_private_key = (
+            f", private_key" f"='{self._private_key[:1]}***{self._private_key[-1:]}'"
+            if self._private_key
+            else ""
+        )
+        private_key_path = (
+            f", private_key_path='{self._private_key_path}'"
+            if self._private_key_path
+            else ""
+        )
+
+        return f"GithubApp({censored_id}{censored_private_key}{private_key_path})"
+
+    @property
+    def private_key(self) -> str:
+        if self._private_key:
+            return self._private_key
+
+        if self._private_key_path:
+            if not Path(self._private_key_path).is_file():
+                raise OgrException(
+                    f"File with the github-app private key "
+                    f"({self._private_key_path}) "
+                    f"does not exist.",
+                )
+            return Path(self._private_key_path).read_text()
+
+        return None
+
+    @property
+    def pygithub_instance(self) -> Optional[github.Github]:
+        # used for backward compatibility with GitUser
+        return None
+
+    @property
+    def integration(self) -> github.GithubIntegration:
+        if not self._integration:
+            self._integration = github.GithubIntegration(self.id, self.private_key)
+        return self._integration
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        if not self.private_key:
+            return None
+
+        # PyGithub 1.58 deprecated get_installation() in favor of get_repo_installation()
+        # that raises an exception on error rather than returning None
+        if hasattr(self.integration, "get_repo_installation"):
+            try:
+                inst_id = self.integration.get_repo_installation(namespace, repo).id
+            except github.GithubException:
+                inst_id = None
+        else:
+            inst_id = self.integration.get_installation(namespace, repo).id
+        # PyGithub<1.52 returned an object for id, with a value attribute,
+        # which was None or an ID.
+        # This was changed in:
+        # https://github.com/PyGithub/PyGithub/commit/61808da15e8e3bcb660acd0e7947326a4a6c0c7a#diff-b8f1ee87df332916352809a397ea259aL54
+        # 'id' is now None or an ID.
+        inst_id = (
+            inst_id
+            if isinstance(inst_id, int) or inst_id is None
+            else inst_id.value  # type: ignore
+        )
+        if not inst_id:
+            raise OgrException(
+                f"No installation ID provided for {namespace}/{repo}: "
+                "please make sure that you provided correct credentials of your GitHub app.",
+            )
+        inst_auth = self.integration.get_access_token(inst_id)  # type: ignore
+        return inst_auth.token
+
+    @staticmethod
+    def try_create(
+        github_app_id: Optional[str] = None,
+        github_app_private_key: Optional[str] = None,
+        github_app_private_key_path: Optional[str] = None,
+        **_,
+    ) -> Optional["GithubApp"]:
+        return (
+            GithubApp(
+                github_app_id,
+                github_app_private_key,
+                github_app_private_key_path,
+            )
+            if github_app_id
+            else None
+        )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubApp +(id: str, private_key: str, private_key_path: str) +
+
+

Represents a token manager for authentication via GitHub App.

+
+ +Expand source code + +
class GithubApp(GithubAuthentication):
+    def __init__(self, id: str, private_key: str, private_key_path: str) -> None:
+        self.id = id
+        self._private_key = private_key
+        self._private_key_path = private_key_path
+
+        self._github: github.Github = None
+        self._integration: github.GithubIntegration = None
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, GithubApp):
+            return False
+
+        return (
+            self.id == o.id  # type: ignore
+            and self._private_key == o._private_key  # type: ignore
+            and self._private_key_path == o._private_key_path  # type: ignore
+        )
+
+    def __str__(self) -> str:
+        censored_id = f"id='{self.id[:1]}***{self.id[-1:]}'"
+        censored_private_key = (
+            f", private_key" f"='{self._private_key[:1]}***{self._private_key[-1:]}'"
+            if self._private_key
+            else ""
+        )
+        private_key_path = (
+            f", private_key_path='{self._private_key_path}'"
+            if self._private_key_path
+            else ""
+        )
+
+        return f"GithubApp({censored_id}{censored_private_key}{private_key_path})"
+
+    @property
+    def private_key(self) -> str:
+        if self._private_key:
+            return self._private_key
+
+        if self._private_key_path:
+            if not Path(self._private_key_path).is_file():
+                raise OgrException(
+                    f"File with the github-app private key "
+                    f"({self._private_key_path}) "
+                    f"does not exist.",
+                )
+            return Path(self._private_key_path).read_text()
+
+        return None
+
+    @property
+    def pygithub_instance(self) -> Optional[github.Github]:
+        # used for backward compatibility with GitUser
+        return None
+
+    @property
+    def integration(self) -> github.GithubIntegration:
+        if not self._integration:
+            self._integration = github.GithubIntegration(self.id, self.private_key)
+        return self._integration
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        if not self.private_key:
+            return None
+
+        # PyGithub 1.58 deprecated get_installation() in favor of get_repo_installation()
+        # that raises an exception on error rather than returning None
+        if hasattr(self.integration, "get_repo_installation"):
+            try:
+                inst_id = self.integration.get_repo_installation(namespace, repo).id
+            except github.GithubException:
+                inst_id = None
+        else:
+            inst_id = self.integration.get_installation(namespace, repo).id
+        # PyGithub<1.52 returned an object for id, with a value attribute,
+        # which was None or an ID.
+        # This was changed in:
+        # https://github.com/PyGithub/PyGithub/commit/61808da15e8e3bcb660acd0e7947326a4a6c0c7a#diff-b8f1ee87df332916352809a397ea259aL54
+        # 'id' is now None or an ID.
+        inst_id = (
+            inst_id
+            if isinstance(inst_id, int) or inst_id is None
+            else inst_id.value  # type: ignore
+        )
+        if not inst_id:
+            raise OgrException(
+                f"No installation ID provided for {namespace}/{repo}: "
+                "please make sure that you provided correct credentials of your GitHub app.",
+            )
+        inst_auth = self.integration.get_access_token(inst_id)  # type: ignore
+        return inst_auth.token
+
+    @staticmethod
+    def try_create(
+        github_app_id: Optional[str] = None,
+        github_app_private_key: Optional[str] = None,
+        github_app_private_key_path: Optional[str] = None,
+        **_,
+    ) -> Optional["GithubApp"]:
+        return (
+            GithubApp(
+                github_app_id,
+                github_app_private_key,
+                github_app_private_key_path,
+            )
+            if github_app_id
+            else None
+        )
+
+

Ancestors

+ +

Instance variables

+
+
var integration : github.GithubIntegration.GithubIntegration
+
+
+
+ +Expand source code + +
@property
+def integration(self) -> github.GithubIntegration:
+    if not self._integration:
+        self._integration = github.GithubIntegration(self.id, self.private_key)
+    return self._integration
+
+
+
var private_key : str
+
+
+
+ +Expand source code + +
@property
+def private_key(self) -> str:
+    if self._private_key:
+        return self._private_key
+
+    if self._private_key_path:
+        if not Path(self._private_key_path).is_file():
+            raise OgrException(
+                f"File with the github-app private key "
+                f"({self._private_key_path}) "
+                f"does not exist.",
+            )
+        return Path(self._private_key_path).read_text()
+
+    return None
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/auth_providers/index.html b/docs/services/github/auth_providers/index.html new file mode 100644 index 00000000..f58230c7 --- /dev/null +++ b/docs/services/github/auth_providers/index.html @@ -0,0 +1,557 @@ + + + + + + +ogr.services.github.auth_providers API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.auth_providers

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from ogr.services.github.auth_providers.abstract import GithubAuthentication
+from ogr.services.github.auth_providers.github_app import GithubApp
+from ogr.services.github.auth_providers.token import TokenAuthentication
+from ogr.services.github.auth_providers.tokman import Tokman
+
+__all__ = [
+    GithubAuthentication.__name__,
+    TokenAuthentication.__name__,
+    GithubApp.__name__,
+    Tokman.__name__,
+]
+
+
+
+

Sub-modules

+
+
ogr.services.github.auth_providers.abstract
+
+
+
+
ogr.services.github.auth_providers.github_app
+
+
+
+
ogr.services.github.auth_providers.token
+
+
+
+
ogr.services.github.auth_providers.tokman
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubApp +(id: str, private_key: str, private_key_path: str) +
+
+

Represents a token manager for authentication via GitHub App.

+
+ +Expand source code + +
class GithubApp(GithubAuthentication):
+    def __init__(self, id: str, private_key: str, private_key_path: str) -> None:
+        self.id = id
+        self._private_key = private_key
+        self._private_key_path = private_key_path
+
+        self._github: github.Github = None
+        self._integration: github.GithubIntegration = None
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, GithubApp):
+            return False
+
+        return (
+            self.id == o.id  # type: ignore
+            and self._private_key == o._private_key  # type: ignore
+            and self._private_key_path == o._private_key_path  # type: ignore
+        )
+
+    def __str__(self) -> str:
+        censored_id = f"id='{self.id[:1]}***{self.id[-1:]}'"
+        censored_private_key = (
+            f", private_key" f"='{self._private_key[:1]}***{self._private_key[-1:]}'"
+            if self._private_key
+            else ""
+        )
+        private_key_path = (
+            f", private_key_path='{self._private_key_path}'"
+            if self._private_key_path
+            else ""
+        )
+
+        return f"GithubApp({censored_id}{censored_private_key}{private_key_path})"
+
+    @property
+    def private_key(self) -> str:
+        if self._private_key:
+            return self._private_key
+
+        if self._private_key_path:
+            if not Path(self._private_key_path).is_file():
+                raise OgrException(
+                    f"File with the github-app private key "
+                    f"({self._private_key_path}) "
+                    f"does not exist.",
+                )
+            return Path(self._private_key_path).read_text()
+
+        return None
+
+    @property
+    def pygithub_instance(self) -> Optional[github.Github]:
+        # used for backward compatibility with GitUser
+        return None
+
+    @property
+    def integration(self) -> github.GithubIntegration:
+        if not self._integration:
+            self._integration = github.GithubIntegration(self.id, self.private_key)
+        return self._integration
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        if not self.private_key:
+            return None
+
+        # PyGithub 1.58 deprecated get_installation() in favor of get_repo_installation()
+        # that raises an exception on error rather than returning None
+        if hasattr(self.integration, "get_repo_installation"):
+            try:
+                inst_id = self.integration.get_repo_installation(namespace, repo).id
+            except github.GithubException:
+                inst_id = None
+        else:
+            inst_id = self.integration.get_installation(namespace, repo).id
+        # PyGithub<1.52 returned an object for id, with a value attribute,
+        # which was None or an ID.
+        # This was changed in:
+        # https://github.com/PyGithub/PyGithub/commit/61808da15e8e3bcb660acd0e7947326a4a6c0c7a#diff-b8f1ee87df332916352809a397ea259aL54
+        # 'id' is now None or an ID.
+        inst_id = (
+            inst_id
+            if isinstance(inst_id, int) or inst_id is None
+            else inst_id.value  # type: ignore
+        )
+        if not inst_id:
+            raise OgrException(
+                f"No installation ID provided for {namespace}/{repo}: "
+                "please make sure that you provided correct credentials of your GitHub app.",
+            )
+        inst_auth = self.integration.get_access_token(inst_id)  # type: ignore
+        return inst_auth.token
+
+    @staticmethod
+    def try_create(
+        github_app_id: Optional[str] = None,
+        github_app_private_key: Optional[str] = None,
+        github_app_private_key_path: Optional[str] = None,
+        **_,
+    ) -> Optional["GithubApp"]:
+        return (
+            GithubApp(
+                github_app_id,
+                github_app_private_key,
+                github_app_private_key_path,
+            )
+            if github_app_id
+            else None
+        )
+
+

Ancestors

+ +

Instance variables

+
+
var integration : github.GithubIntegration.GithubIntegration
+
+
+
+ +Expand source code + +
@property
+def integration(self) -> github.GithubIntegration:
+    if not self._integration:
+        self._integration = github.GithubIntegration(self.id, self.private_key)
+    return self._integration
+
+
+
var private_key : str
+
+
+
+ +Expand source code + +
@property
+def private_key(self) -> str:
+    if self._private_key:
+        return self._private_key
+
+    if self._private_key_path:
+        if not Path(self._private_key_path).is_file():
+            raise OgrException(
+                f"File with the github-app private key "
+                f"({self._private_key_path}) "
+                f"does not exist.",
+            )
+        return Path(self._private_key_path).read_text()
+
+    return None
+
+
+
+

Inherited members

+ +
+
+class GithubAuthentication +
+
+

Represents a token manager for authentication via GitHub App.

+
+ +Expand source code + +
class GithubAuthentication:
+    """
+    Represents a token manager for authentication via GitHub App.
+    """
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        """
+        Get a GitHub token for requested repository.
+
+        Args:
+            namespace: Namespace of the repository.
+            repo: Name of the repository.
+
+        Returns:
+            A token that can be used in PyGithub instance for authentication.
+        """
+        raise NotImplementedError()
+
+    @property
+    def pygithub_instance(self) -> "github.Github":
+        """
+        Returns:
+            Generic PyGithub instance. Used for `GitUser` for example.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def try_create(**kwargs) -> Optional["GithubAuthentication"]:
+        """
+        Tries to construct authentication object from provided keyword arguments.
+
+        Returns:
+            `GithubAuthentication` object or `None` if the creation was not
+            successful.
+        """
+        raise NotImplementedError()
+
+

Subclasses

+ +

Static methods

+
+
+def try_create(**kwargs) ‑> Optional[GithubAuthentication] +
+
+

Tries to construct authentication object from provided keyword arguments.

+

Returns

+

GithubAuthentication object or None if the creation was not +successful.

+
+ +Expand source code + +
@staticmethod
+def try_create(**kwargs) -> Optional["GithubAuthentication"]:
+    """
+    Tries to construct authentication object from provided keyword arguments.
+
+    Returns:
+        `GithubAuthentication` object or `None` if the creation was not
+        successful.
+    """
+    raise NotImplementedError()
+
+
+
+

Instance variables

+
+
var pygithub_instance : github.MainClass.Github
+
+

Returns

+

Generic PyGithub instance. Used for GitUser for example.

+
+ +Expand source code + +
@property
+def pygithub_instance(self) -> "github.Github":
+    """
+    Returns:
+        Generic PyGithub instance. Used for `GitUser` for example.
+    """
+    raise NotImplementedError()
+
+
+
+

Methods

+
+
+def get_token(self, namespace: str, repo: str) ‑> str +
+
+

Get a GitHub token for requested repository.

+

Args

+
+
namespace
+
Namespace of the repository.
+
repo
+
Name of the repository.
+
+

Returns

+

A token that can be used in PyGithub instance for authentication.

+
+ +Expand source code + +
def get_token(self, namespace: str, repo: str) -> str:
+    """
+    Get a GitHub token for requested repository.
+
+    Args:
+        namespace: Namespace of the repository.
+        repo: Name of the repository.
+
+    Returns:
+        A token that can be used in PyGithub instance for authentication.
+    """
+    raise NotImplementedError()
+
+
+
+
+
+class TokenAuthentication +(token: str, max_retries: Union[int, urllib3.util.retry.Retry] = 0, **_) +
+
+

Represents a token manager for authentication via GitHub App.

+
+ +Expand source code + +
class TokenAuthentication(GithubAuthentication):
+    def __init__(self, token: str, max_retries: Union[int, Retry] = 0, **_) -> None:
+        self._token = token
+        self._pygithub_instance = github.Github(login_or_token=token, retry=max_retries)
+
+    def __eq__(self, o: object) -> bool:
+        return issubclass(o.__class__, TokenAuthentication) and (
+            self._token == o._token  # type: ignore
+        )
+
+    def __str__(self) -> str:
+        censored_token = (
+            f"token='{self._token[:1]}***{self._token[-1:]}'" if self._token else ""
+        )
+        return f"Token({censored_token})"
+
+    @property
+    def pygithub_instance(self) -> github.Github:
+        return self._pygithub_instance
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        return self._token
+
+    @staticmethod
+    def try_create(
+        token: Optional[str] = None,
+        max_retries: Union[int, Retry] = 0,
+        **_,
+    ) -> Optional["TokenAuthentication"]:
+        return TokenAuthentication(token, max_retries=max_retries)
+
+

Ancestors

+ +

Inherited members

+ +
+
+class Tokman +(instance_url: str) +
+
+

Represents a token manager for authentication via GitHub App.

+
+ +Expand source code + +
class Tokman(GithubAuthentication):
+    def __init__(self, instance_url: str):
+        self._instance_url = instance_url
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, Tokman):
+            return False
+
+        return self._instance_url == o._instance_url  # type: ignore
+
+    def __str__(self) -> str:
+        return f"Tokman(instance_url='{self._instance_url}')"
+
+    @property
+    def pygithub_instance(self) -> Optional[github.Github]:
+        # used for backward compatibility with GitUser
+        return None
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        response = requests.get(f"{self._instance_url}/api/{namespace}/{repo}")
+
+        if not response.ok:
+            if response.status_code == 400:
+                raise GithubAppNotInstalledError(response.text)
+
+            cls = OgrNetworkError if response.status_code >= 500 else OgrException
+            raise cls(
+                f"Couldn't retrieve token from Tokman: ({response.status_code}) {response.text}",
+            )
+
+        return response.json().get("access_token", None)
+
+    @staticmethod
+    def try_create(
+        tokman_instance_url: Optional[str] = None,
+        **_,
+    ) -> Optional["Tokman"]:
+        return Tokman(tokman_instance_url) if tokman_instance_url else None
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/auth_providers/token.html b/docs/services/github/auth_providers/token.html new file mode 100644 index 00000000..90889fd6 --- /dev/null +++ b/docs/services/github/auth_providers/token.html @@ -0,0 +1,165 @@ + + + + + + +ogr.services.github.auth_providers.token API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.auth_providers.token

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from typing import Optional, Union
+
+import github
+from urllib3.util import Retry
+
+from ogr.services.github.auth_providers.abstract import GithubAuthentication
+
+
+class TokenAuthentication(GithubAuthentication):
+    def __init__(self, token: str, max_retries: Union[int, Retry] = 0, **_) -> None:
+        self._token = token
+        self._pygithub_instance = github.Github(login_or_token=token, retry=max_retries)
+
+    def __eq__(self, o: object) -> bool:
+        return issubclass(o.__class__, TokenAuthentication) and (
+            self._token == o._token  # type: ignore
+        )
+
+    def __str__(self) -> str:
+        censored_token = (
+            f"token='{self._token[:1]}***{self._token[-1:]}'" if self._token else ""
+        )
+        return f"Token({censored_token})"
+
+    @property
+    def pygithub_instance(self) -> github.Github:
+        return self._pygithub_instance
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        return self._token
+
+    @staticmethod
+    def try_create(
+        token: Optional[str] = None,
+        max_retries: Union[int, Retry] = 0,
+        **_,
+    ) -> Optional["TokenAuthentication"]:
+        return TokenAuthentication(token, max_retries=max_retries)
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class TokenAuthentication +(token: str, max_retries: Union[int, urllib3.util.retry.Retry] = 0, **_) +
+
+

Represents a token manager for authentication via GitHub App.

+
+ +Expand source code + +
class TokenAuthentication(GithubAuthentication):
+    def __init__(self, token: str, max_retries: Union[int, Retry] = 0, **_) -> None:
+        self._token = token
+        self._pygithub_instance = github.Github(login_or_token=token, retry=max_retries)
+
+    def __eq__(self, o: object) -> bool:
+        return issubclass(o.__class__, TokenAuthentication) and (
+            self._token == o._token  # type: ignore
+        )
+
+    def __str__(self) -> str:
+        censored_token = (
+            f"token='{self._token[:1]}***{self._token[-1:]}'" if self._token else ""
+        )
+        return f"Token({censored_token})"
+
+    @property
+    def pygithub_instance(self) -> github.Github:
+        return self._pygithub_instance
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        return self._token
+
+    @staticmethod
+    def try_create(
+        token: Optional[str] = None,
+        max_retries: Union[int, Retry] = 0,
+        **_,
+    ) -> Optional["TokenAuthentication"]:
+        return TokenAuthentication(token, max_retries=max_retries)
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/auth_providers/tokman.html b/docs/services/github/auth_providers/tokman.html new file mode 100644 index 00000000..d1c4257d --- /dev/null +++ b/docs/services/github/auth_providers/tokman.html @@ -0,0 +1,182 @@ + + + + + + +ogr.services.github.auth_providers.tokman API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.auth_providers.tokman

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from typing import Optional
+
+import github
+import requests
+
+from ogr.exceptions import GithubAppNotInstalledError, OgrException, OgrNetworkError
+from ogr.services.github.auth_providers.abstract import GithubAuthentication
+
+
+class Tokman(GithubAuthentication):
+    def __init__(self, instance_url: str):
+        self._instance_url = instance_url
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, Tokman):
+            return False
+
+        return self._instance_url == o._instance_url  # type: ignore
+
+    def __str__(self) -> str:
+        return f"Tokman(instance_url='{self._instance_url}')"
+
+    @property
+    def pygithub_instance(self) -> Optional[github.Github]:
+        # used for backward compatibility with GitUser
+        return None
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        response = requests.get(f"{self._instance_url}/api/{namespace}/{repo}")
+
+        if not response.ok:
+            if response.status_code == 400:
+                raise GithubAppNotInstalledError(response.text)
+
+            cls = OgrNetworkError if response.status_code >= 500 else OgrException
+            raise cls(
+                f"Couldn't retrieve token from Tokman: ({response.status_code}) {response.text}",
+            )
+
+        return response.json().get("access_token", None)
+
+    @staticmethod
+    def try_create(
+        tokman_instance_url: Optional[str] = None,
+        **_,
+    ) -> Optional["Tokman"]:
+        return Tokman(tokman_instance_url) if tokman_instance_url else None
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Tokman +(instance_url: str) +
+
+

Represents a token manager for authentication via GitHub App.

+
+ +Expand source code + +
class Tokman(GithubAuthentication):
+    def __init__(self, instance_url: str):
+        self._instance_url = instance_url
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, Tokman):
+            return False
+
+        return self._instance_url == o._instance_url  # type: ignore
+
+    def __str__(self) -> str:
+        return f"Tokman(instance_url='{self._instance_url}')"
+
+    @property
+    def pygithub_instance(self) -> Optional[github.Github]:
+        # used for backward compatibility with GitUser
+        return None
+
+    def get_token(self, namespace: str, repo: str) -> str:
+        response = requests.get(f"{self._instance_url}/api/{namespace}/{repo}")
+
+        if not response.ok:
+            if response.status_code == 400:
+                raise GithubAppNotInstalledError(response.text)
+
+            cls = OgrNetworkError if response.status_code >= 500 else OgrException
+            raise cls(
+                f"Couldn't retrieve token from Tokman: ({response.status_code}) {response.text}",
+            )
+
+        return response.json().get("access_token", None)
+
+    @staticmethod
+    def try_create(
+        tokman_instance_url: Optional[str] = None,
+        **_,
+    ) -> Optional["Tokman"]:
+        return Tokman(tokman_instance_url) if tokman_instance_url else None
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/check_run.html b/docs/services/github/check_run.html new file mode 100644 index 00000000..d4315367 --- /dev/null +++ b/docs/services/github/check_run.html @@ -0,0 +1,1483 @@ + + + + + + +ogr.services.github.check_run API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.check_run

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from enum import Enum
+from typing import Any, Optional, Union
+
+from github.CheckRun import CheckRun
+from github.CheckRunOutput import CheckRunOutput
+from github.GithubApp import GithubApp
+from github.GithubObject import NotSet
+
+from ogr.abstract import OgrAbstractClass
+from ogr.exceptions import OperationNotSupported
+from ogr.services import github as ogr_github
+
+GithubCheckRunOutput = dict[str, Union[str, list[dict[str, Union[str, int]]]]]
+
+
+class GithubCheckRunStatus(Enum):
+    """
+    Represents statuses GitHub check run can have.
+    """
+
+    queued = "queued"
+    in_progress = "in_progress"
+    completed = "completed"
+
+
+class GithubCheckRunResult(Enum):
+    """
+    Represents conclusion/result of the GitHub check run.
+    """
+
+    action_required = "action_required"
+    cancelled = "cancelled"
+    failure = "failure"
+    neutral = "neutral"
+    success = "success"
+    skipped = "skipped"
+    stale = "stale"
+    timed_out = "timed_out"
+
+
+def value_or_NotSet(value: Optional[Any]) -> Any:
+    """
+    Wrapper for PyGithub, allows us to transform `None` into PyGithub's `NotSet`.
+
+    Args:
+        value: Value that can be None.
+
+    Returns:
+        If value is not None, value is returned; NotSet otherwise.
+    """
+    return value if value is not None else NotSet
+
+
+def create_github_check_run_output(
+    title: str,
+    summary: str,
+    text: Optional[str] = None,
+    annotations: Optional[list[dict[str, Union[str, int]]]] = None,
+) -> GithubCheckRunOutput:
+    """
+    Helper function for constructing valid GitHub output for check run.
+
+    Args:
+        title: Title of the output.
+        summary: Summary of the output.
+        text: Optional text for the output. Can be markdown formatted.
+
+            Defaults to `None`.
+        annotations: Optional annotations that are tied to source code.
+
+    Returns:
+        Dictionary that represents valid output for check run.
+    """
+    output: GithubCheckRunOutput = {
+        "title": title,
+        "summary": summary,
+    }
+
+    if text is not None:
+        output["text"] = text
+
+    if annotations is not None:
+        output["annotations"] = annotations
+
+    return output
+
+
+class GithubCheckRun(OgrAbstractClass):
+    def __init__(
+        self,
+        project: "ogr_github.GithubProject",
+        raw_check_run: CheckRun,
+    ) -> None:
+        self.raw_check_run = raw_check_run
+        self.project = project
+
+    def __str__(self) -> str:
+        return (
+            f"GithubCheckRun(project={self.project}, name='{self.name}', "
+            f"commit_sha='{self.commit_sha}', "
+            f"url='{self.url}', "
+            f"external_id='{self.external_id}', "
+            f"status={self.status.name}, "
+            f"started_at={self.started_at}, "
+            f"conclusion={self.conclusion}, "
+            f"completed_at={self.completed_at}, "
+            f"output={self.output}, "
+            f"app={self.app})"
+        )
+
+    @property
+    def name(self) -> str:
+        """Name of the check run."""
+        return self.raw_check_run.name
+
+    @name.setter
+    def name(self, name: str) -> None:
+        self.raw_check_run.edit(name=name)
+
+    @property
+    def commit_sha(self) -> str:
+        """Commit SHA that check run is related to."""
+        return self.raw_check_run.head_sha
+
+    @property
+    def url(self) -> Optional[str]:
+        """URL with additional details."""
+        return self.raw_check_run.details_url
+
+    @url.setter
+    def url(self, url: str) -> None:
+        self.raw_check_run.edit(details_url=url)
+
+    @property
+    def external_id(self) -> Optional[str]:
+        """External ID that can be used internally by the integrator."""
+        return self.raw_check_run.external_id
+
+    @external_id.setter
+    def external_id(self, external_id: str) -> None:
+        self.raw_check_run.edit(external_id=external_id)
+
+    @property
+    def status(self) -> GithubCheckRunStatus:
+        """Current status of the check run."""
+        return GithubCheckRunStatus(self.raw_check_run.status)
+
+    @property
+    def started_at(self) -> Optional[datetime.datetime]:
+        """Timestamp of start of the check run."""
+        return self.raw_check_run.started_at
+
+    @started_at.setter
+    def started_at(self, started_at: datetime.datetime) -> None:
+        self.raw_check_run.edit(started_at=started_at)
+
+    @property
+    def conclusion(self) -> Optional[GithubCheckRunResult]:
+        """Conclusion/result of the check run."""
+        return (
+            GithubCheckRunResult(self.raw_check_run.conclusion)
+            if self.raw_check_run.conclusion
+            else None
+        )
+
+    @property
+    def completed_at(self) -> Optional[datetime.datetime]:
+        """Timestamp of completion of the check run."""
+        return self.raw_check_run.completed_at
+
+    @property
+    def output(self) -> CheckRunOutput:
+        """Output of the check run."""
+        return self.raw_check_run.output
+
+    @output.setter
+    def output(self, output: GithubCheckRunOutput) -> None:
+        self.raw_check_run.edit(output=output)
+
+    @property
+    def app(self) -> GithubApp:
+        """Github App of the check run."""
+        return self.raw_check_run.app
+
+    def change_status(
+        self,
+        status: Optional[GithubCheckRunStatus] = None,
+        completed_at: Optional[datetime.datetime] = None,
+        conclusion: Optional[GithubCheckRunResult] = None,
+    ) -> None:
+        """
+        Changes the status of the check run and checks the validity of new state.
+
+        Args:
+            status: Status of the check run to be set. If set to completed, you
+                must provide conclusion.
+
+                Defaults to `None`.
+            completed_at: Timestamp of completion of the check run. If set, you
+                must provide conclusion.
+
+                Defaults to `None`.
+            conclusion: Conclusion/result of the check run. If only conclusion
+                is set, status is automatically set to completed.
+
+                Defaults to `None`.
+
+        Raises:
+            OperationNotSupported, if given completed or timestamp of completed
+                without conclusion.
+        """
+        if not (status or completed_at or conclusion):
+            return
+
+        if (
+            status == GithubCheckRunStatus.completed or completed_at
+        ) and conclusion is None:
+            raise OperationNotSupported(
+                "When provided completed status or completed at,"
+                " you need to provide conclusion.",
+            )
+
+        self.raw_check_run.edit(
+            status=value_or_NotSet(status.name if status else None),
+            conclusion=value_or_NotSet(conclusion.name if conclusion else None),
+            completed_at=value_or_NotSet(completed_at),
+        )
+
+    @staticmethod
+    def get_list(
+        project: "ogr_github.GithubProject",
+        commit_sha: str,
+        name: Optional[str] = None,
+        status: Optional[GithubCheckRunStatus] = None,
+    ) -> list["GithubCheckRun"]:
+        """
+        Returns list of GitHub check runs.
+
+        Args:
+            project: Project from which the check runs are retrieved.
+            commit_sha: Commit to which are the check runs related to.
+            name: Name of the check run for filtering.
+
+                Defaults to `None`, no filtering.
+            status: Status of the check runs to be returned.
+
+                Defaults to `None`, no filtering.
+
+        Returns:
+            List of the check runs.
+        """
+        check_runs = project.github_repo.get_commit(commit_sha).get_check_runs(
+            check_name=value_or_NotSet(name),
+            status=value_or_NotSet(status.name if status else None),
+        )
+
+        return [GithubCheckRun(project, run) for run in check_runs]
+
+    @staticmethod
+    def get(
+        project: "ogr_github.GithubProject",
+        check_run_id: Optional[int] = None,
+        commit_sha: Optional[str] = None,
+    ) -> Optional["GithubCheckRun"]:
+        """
+        Retrieves GitHub check run as ogr object.
+
+        Args:
+            project: Project from which the check run is retrieved.
+            check_run_id: Check run ID.
+
+                Defaults to `None`, i.e. is not used for query.
+            commit_sha: Commit SHA from which the check run is to be retrieved.
+                If set, returns latest check run for the commit.
+
+                Defaults to `None`, i.e. is not used for query.
+
+        Returns:
+            GithubCheckRun object or `None` if no check run is found.
+
+        Raises:
+            OperationNotSupported, in case there is no parameter for query set
+                or both are set.
+        """
+        if check_run_id is not None and commit_sha:
+            raise OperationNotSupported(
+                "Cannot retrieve check run by both ID and commit hash",
+            )
+
+        if not (check_run_id is not None or commit_sha):
+            raise OperationNotSupported("Cannot retrieve check run by no criteria")
+
+        if check_run_id is not None:
+            return GithubCheckRun(
+                project,
+                project.github_repo.get_check_run(check_run_id),
+            )
+
+        check_runs = project.github_repo.get_commit(commit_sha).get_check_runs()
+        if check_runs.totalCount == 0:
+            return None
+        return GithubCheckRun(project, check_runs[0])
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        name: str,
+        commit_sha: str,
+        url: Optional[str] = None,
+        external_id: Optional[str] = None,
+        status: GithubCheckRunStatus = GithubCheckRunStatus.queued,
+        started_at: Optional[datetime.datetime] = None,
+        conclusion: Optional[GithubCheckRunResult] = None,
+        completed_at: Optional[datetime.datetime] = None,
+        output: Optional[GithubCheckRunOutput] = None,
+        actions: Optional[list[dict[str, str]]] = None,
+    ) -> "GithubCheckRun":
+        """
+        Creates new check run.
+
+        Args:
+            project: Project where the check run is to be created.
+            name: Name of the check run.
+            commit_sha: Hash of the commit that check run is related to.
+            url: URL with details of the run.
+
+                Defaults to `None`.
+            external_id: External ID that can be used internally by integrator.
+
+                Defaults to `None`.
+            status: Status of the check run.
+
+                Defaults to queued.
+            started_at: Timestamp of starting the check run.
+
+                Defaults to `None`.
+            conclusion: Conclusion of the check run. Should be set with status
+                completed.
+
+                Defaults to `None`.
+            completed_at: Timestamp of completion of the check run. If set, you
+                must provide conclusion.
+
+                Defaults to `None`.
+            output: Output of the check run.
+            actions: List of possible follow-up actions for the check run.
+
+        Returns:
+            Created check run object.
+
+        Raises:
+            OperationNotSupported, if given completed status or completion
+                timestamp and no conclusion.
+        """
+
+        if (
+            completed_at or status == GithubCheckRunStatus.completed
+        ) and conclusion is None:
+            raise OperationNotSupported(
+                "When provided completed_at or completed status, "
+                "you need to provide conclusion.",
+            )
+
+        created_check_run = project.github_repo.create_check_run(
+            name=name,
+            head_sha=commit_sha,
+            details_url=value_or_NotSet(url),
+            external_id=value_or_NotSet(external_id),
+            status=status.name,
+            started_at=value_or_NotSet(started_at),
+            conclusion=value_or_NotSet(conclusion.name if conclusion else None),
+            completed_at=value_or_NotSet(completed_at),
+            output=value_or_NotSet(output),
+            actions=value_or_NotSet(actions),
+        )
+
+        return GithubCheckRun(project, created_check_run)
+
+
+
+
+
+
+
+

Functions

+
+
+def create_github_check_run_output(title: str, summary: str, text: Optional[str] = None, annotations: Optional[list[dict[str, typing.Union[str, int]]]] = None) ‑> dict[str, typing.Union[str, list[dict[str, typing.Union[str, int]]]]] +
+
+

Helper function for constructing valid GitHub output for check run.

+

Args

+
+
title
+
Title of the output.
+
summary
+
Summary of the output.
+
text
+
+

Optional text for the output. Can be markdown formatted.

+

Defaults to None.

+
+
annotations
+
Optional annotations that are tied to source code.
+
+

Returns

+

Dictionary that represents valid output for check run.

+
+ +Expand source code + +
def create_github_check_run_output(
+    title: str,
+    summary: str,
+    text: Optional[str] = None,
+    annotations: Optional[list[dict[str, Union[str, int]]]] = None,
+) -> GithubCheckRunOutput:
+    """
+    Helper function for constructing valid GitHub output for check run.
+
+    Args:
+        title: Title of the output.
+        summary: Summary of the output.
+        text: Optional text for the output. Can be markdown formatted.
+
+            Defaults to `None`.
+        annotations: Optional annotations that are tied to source code.
+
+    Returns:
+        Dictionary that represents valid output for check run.
+    """
+    output: GithubCheckRunOutput = {
+        "title": title,
+        "summary": summary,
+    }
+
+    if text is not None:
+        output["text"] = text
+
+    if annotations is not None:
+        output["annotations"] = annotations
+
+    return output
+
+
+
+def value_or_NotSet(value: Optional[Any]) ‑> Any +
+
+

Wrapper for PyGithub, allows us to transform None into PyGithub's NotSet.

+

Args

+
+
value
+
Value that can be None.
+
+

Returns

+

If value is not None, value is returned; NotSet otherwise.

+
+ +Expand source code + +
def value_or_NotSet(value: Optional[Any]) -> Any:
+    """
+    Wrapper for PyGithub, allows us to transform `None` into PyGithub's `NotSet`.
+
+    Args:
+        value: Value that can be None.
+
+    Returns:
+        If value is not None, value is returned; NotSet otherwise.
+    """
+    return value if value is not None else NotSet
+
+
+
+
+
+

Classes

+
+
+class GithubCheckRun +(project: ogr_github.GithubProject, raw_check_run: github.CheckRun.CheckRun) +
+
+
+
+ +Expand source code + +
class GithubCheckRun(OgrAbstractClass):
+    def __init__(
+        self,
+        project: "ogr_github.GithubProject",
+        raw_check_run: CheckRun,
+    ) -> None:
+        self.raw_check_run = raw_check_run
+        self.project = project
+
+    def __str__(self) -> str:
+        return (
+            f"GithubCheckRun(project={self.project}, name='{self.name}', "
+            f"commit_sha='{self.commit_sha}', "
+            f"url='{self.url}', "
+            f"external_id='{self.external_id}', "
+            f"status={self.status.name}, "
+            f"started_at={self.started_at}, "
+            f"conclusion={self.conclusion}, "
+            f"completed_at={self.completed_at}, "
+            f"output={self.output}, "
+            f"app={self.app})"
+        )
+
+    @property
+    def name(self) -> str:
+        """Name of the check run."""
+        return self.raw_check_run.name
+
+    @name.setter
+    def name(self, name: str) -> None:
+        self.raw_check_run.edit(name=name)
+
+    @property
+    def commit_sha(self) -> str:
+        """Commit SHA that check run is related to."""
+        return self.raw_check_run.head_sha
+
+    @property
+    def url(self) -> Optional[str]:
+        """URL with additional details."""
+        return self.raw_check_run.details_url
+
+    @url.setter
+    def url(self, url: str) -> None:
+        self.raw_check_run.edit(details_url=url)
+
+    @property
+    def external_id(self) -> Optional[str]:
+        """External ID that can be used internally by the integrator."""
+        return self.raw_check_run.external_id
+
+    @external_id.setter
+    def external_id(self, external_id: str) -> None:
+        self.raw_check_run.edit(external_id=external_id)
+
+    @property
+    def status(self) -> GithubCheckRunStatus:
+        """Current status of the check run."""
+        return GithubCheckRunStatus(self.raw_check_run.status)
+
+    @property
+    def started_at(self) -> Optional[datetime.datetime]:
+        """Timestamp of start of the check run."""
+        return self.raw_check_run.started_at
+
+    @started_at.setter
+    def started_at(self, started_at: datetime.datetime) -> None:
+        self.raw_check_run.edit(started_at=started_at)
+
+    @property
+    def conclusion(self) -> Optional[GithubCheckRunResult]:
+        """Conclusion/result of the check run."""
+        return (
+            GithubCheckRunResult(self.raw_check_run.conclusion)
+            if self.raw_check_run.conclusion
+            else None
+        )
+
+    @property
+    def completed_at(self) -> Optional[datetime.datetime]:
+        """Timestamp of completion of the check run."""
+        return self.raw_check_run.completed_at
+
+    @property
+    def output(self) -> CheckRunOutput:
+        """Output of the check run."""
+        return self.raw_check_run.output
+
+    @output.setter
+    def output(self, output: GithubCheckRunOutput) -> None:
+        self.raw_check_run.edit(output=output)
+
+    @property
+    def app(self) -> GithubApp:
+        """Github App of the check run."""
+        return self.raw_check_run.app
+
+    def change_status(
+        self,
+        status: Optional[GithubCheckRunStatus] = None,
+        completed_at: Optional[datetime.datetime] = None,
+        conclusion: Optional[GithubCheckRunResult] = None,
+    ) -> None:
+        """
+        Changes the status of the check run and checks the validity of new state.
+
+        Args:
+            status: Status of the check run to be set. If set to completed, you
+                must provide conclusion.
+
+                Defaults to `None`.
+            completed_at: Timestamp of completion of the check run. If set, you
+                must provide conclusion.
+
+                Defaults to `None`.
+            conclusion: Conclusion/result of the check run. If only conclusion
+                is set, status is automatically set to completed.
+
+                Defaults to `None`.
+
+        Raises:
+            OperationNotSupported, if given completed or timestamp of completed
+                without conclusion.
+        """
+        if not (status or completed_at or conclusion):
+            return
+
+        if (
+            status == GithubCheckRunStatus.completed or completed_at
+        ) and conclusion is None:
+            raise OperationNotSupported(
+                "When provided completed status or completed at,"
+                " you need to provide conclusion.",
+            )
+
+        self.raw_check_run.edit(
+            status=value_or_NotSet(status.name if status else None),
+            conclusion=value_or_NotSet(conclusion.name if conclusion else None),
+            completed_at=value_or_NotSet(completed_at),
+        )
+
+    @staticmethod
+    def get_list(
+        project: "ogr_github.GithubProject",
+        commit_sha: str,
+        name: Optional[str] = None,
+        status: Optional[GithubCheckRunStatus] = None,
+    ) -> list["GithubCheckRun"]:
+        """
+        Returns list of GitHub check runs.
+
+        Args:
+            project: Project from which the check runs are retrieved.
+            commit_sha: Commit to which are the check runs related to.
+            name: Name of the check run for filtering.
+
+                Defaults to `None`, no filtering.
+            status: Status of the check runs to be returned.
+
+                Defaults to `None`, no filtering.
+
+        Returns:
+            List of the check runs.
+        """
+        check_runs = project.github_repo.get_commit(commit_sha).get_check_runs(
+            check_name=value_or_NotSet(name),
+            status=value_or_NotSet(status.name if status else None),
+        )
+
+        return [GithubCheckRun(project, run) for run in check_runs]
+
+    @staticmethod
+    def get(
+        project: "ogr_github.GithubProject",
+        check_run_id: Optional[int] = None,
+        commit_sha: Optional[str] = None,
+    ) -> Optional["GithubCheckRun"]:
+        """
+        Retrieves GitHub check run as ogr object.
+
+        Args:
+            project: Project from which the check run is retrieved.
+            check_run_id: Check run ID.
+
+                Defaults to `None`, i.e. is not used for query.
+            commit_sha: Commit SHA from which the check run is to be retrieved.
+                If set, returns latest check run for the commit.
+
+                Defaults to `None`, i.e. is not used for query.
+
+        Returns:
+            GithubCheckRun object or `None` if no check run is found.
+
+        Raises:
+            OperationNotSupported, in case there is no parameter for query set
+                or both are set.
+        """
+        if check_run_id is not None and commit_sha:
+            raise OperationNotSupported(
+                "Cannot retrieve check run by both ID and commit hash",
+            )
+
+        if not (check_run_id is not None or commit_sha):
+            raise OperationNotSupported("Cannot retrieve check run by no criteria")
+
+        if check_run_id is not None:
+            return GithubCheckRun(
+                project,
+                project.github_repo.get_check_run(check_run_id),
+            )
+
+        check_runs = project.github_repo.get_commit(commit_sha).get_check_runs()
+        if check_runs.totalCount == 0:
+            return None
+        return GithubCheckRun(project, check_runs[0])
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        name: str,
+        commit_sha: str,
+        url: Optional[str] = None,
+        external_id: Optional[str] = None,
+        status: GithubCheckRunStatus = GithubCheckRunStatus.queued,
+        started_at: Optional[datetime.datetime] = None,
+        conclusion: Optional[GithubCheckRunResult] = None,
+        completed_at: Optional[datetime.datetime] = None,
+        output: Optional[GithubCheckRunOutput] = None,
+        actions: Optional[list[dict[str, str]]] = None,
+    ) -> "GithubCheckRun":
+        """
+        Creates new check run.
+
+        Args:
+            project: Project where the check run is to be created.
+            name: Name of the check run.
+            commit_sha: Hash of the commit that check run is related to.
+            url: URL with details of the run.
+
+                Defaults to `None`.
+            external_id: External ID that can be used internally by integrator.
+
+                Defaults to `None`.
+            status: Status of the check run.
+
+                Defaults to queued.
+            started_at: Timestamp of starting the check run.
+
+                Defaults to `None`.
+            conclusion: Conclusion of the check run. Should be set with status
+                completed.
+
+                Defaults to `None`.
+            completed_at: Timestamp of completion of the check run. If set, you
+                must provide conclusion.
+
+                Defaults to `None`.
+            output: Output of the check run.
+            actions: List of possible follow-up actions for the check run.
+
+        Returns:
+            Created check run object.
+
+        Raises:
+            OperationNotSupported, if given completed status or completion
+                timestamp and no conclusion.
+        """
+
+        if (
+            completed_at or status == GithubCheckRunStatus.completed
+        ) and conclusion is None:
+            raise OperationNotSupported(
+                "When provided completed_at or completed status, "
+                "you need to provide conclusion.",
+            )
+
+        created_check_run = project.github_repo.create_check_run(
+            name=name,
+            head_sha=commit_sha,
+            details_url=value_or_NotSet(url),
+            external_id=value_or_NotSet(external_id),
+            status=status.name,
+            started_at=value_or_NotSet(started_at),
+            conclusion=value_or_NotSet(conclusion.name if conclusion else None),
+            completed_at=value_or_NotSet(completed_at),
+            output=value_or_NotSet(output),
+            actions=value_or_NotSet(actions),
+        )
+
+        return GithubCheckRun(project, created_check_run)
+
+

Ancestors

+ +

Static methods

+
+
+def create(project: ogr_github.GithubProject, name: str, commit_sha: str, url: Optional[str] = None, external_id: Optional[str] = None, status: GithubCheckRunStatus = GithubCheckRunStatus.queued, started_at: Optional[datetime.datetime] = None, conclusion: Optional[GithubCheckRunResult] = None, completed_at: Optional[datetime.datetime] = None, output: Optional[dict[str, typing.Union[str, list[dict[str, typing.Union[str, int]]]]]] = None, actions: Optional[list[dict[str, str]]] = None) ‑> GithubCheckRun +
+
+

Creates new check run.

+

Args

+
+
project
+
Project where the check run is to be created.
+
name
+
Name of the check run.
+
commit_sha
+
Hash of the commit that check run is related to.
+
url
+
+

URL with details of the run.

+

Defaults to None.

+
+
external_id
+
+

External ID that can be used internally by integrator.

+

Defaults to None.

+
+
status
+
+

Status of the check run.

+

Defaults to queued.

+
+
started_at
+
+

Timestamp of starting the check run.

+

Defaults to None.

+
+
conclusion
+
+

Conclusion of the check run. Should be set with status +completed.

+

Defaults to None.

+
+
completed_at
+
+

Timestamp of completion of the check run. If set, you +must provide conclusion.

+

Defaults to None.

+
+
output
+
Output of the check run.
+
actions
+
List of possible follow-up actions for the check run.
+
+

Returns

+

Created check run object.

+

Raises

+

OperationNotSupported, if given completed status or completion +timestamp and no conclusion.

+
+ +Expand source code + +
@staticmethod
+def create(
+    project: "ogr_github.GithubProject",
+    name: str,
+    commit_sha: str,
+    url: Optional[str] = None,
+    external_id: Optional[str] = None,
+    status: GithubCheckRunStatus = GithubCheckRunStatus.queued,
+    started_at: Optional[datetime.datetime] = None,
+    conclusion: Optional[GithubCheckRunResult] = None,
+    completed_at: Optional[datetime.datetime] = None,
+    output: Optional[GithubCheckRunOutput] = None,
+    actions: Optional[list[dict[str, str]]] = None,
+) -> "GithubCheckRun":
+    """
+    Creates new check run.
+
+    Args:
+        project: Project where the check run is to be created.
+        name: Name of the check run.
+        commit_sha: Hash of the commit that check run is related to.
+        url: URL with details of the run.
+
+            Defaults to `None`.
+        external_id: External ID that can be used internally by integrator.
+
+            Defaults to `None`.
+        status: Status of the check run.
+
+            Defaults to queued.
+        started_at: Timestamp of starting the check run.
+
+            Defaults to `None`.
+        conclusion: Conclusion of the check run. Should be set with status
+            completed.
+
+            Defaults to `None`.
+        completed_at: Timestamp of completion of the check run. If set, you
+            must provide conclusion.
+
+            Defaults to `None`.
+        output: Output of the check run.
+        actions: List of possible follow-up actions for the check run.
+
+    Returns:
+        Created check run object.
+
+    Raises:
+        OperationNotSupported, if given completed status or completion
+            timestamp and no conclusion.
+    """
+
+    if (
+        completed_at or status == GithubCheckRunStatus.completed
+    ) and conclusion is None:
+        raise OperationNotSupported(
+            "When provided completed_at or completed status, "
+            "you need to provide conclusion.",
+        )
+
+    created_check_run = project.github_repo.create_check_run(
+        name=name,
+        head_sha=commit_sha,
+        details_url=value_or_NotSet(url),
+        external_id=value_or_NotSet(external_id),
+        status=status.name,
+        started_at=value_or_NotSet(started_at),
+        conclusion=value_or_NotSet(conclusion.name if conclusion else None),
+        completed_at=value_or_NotSet(completed_at),
+        output=value_or_NotSet(output),
+        actions=value_or_NotSet(actions),
+    )
+
+    return GithubCheckRun(project, created_check_run)
+
+
+
+def get(project: ogr_github.GithubProject, check_run_id: Optional[int] = None, commit_sha: Optional[str] = None) ‑> Optional[GithubCheckRun] +
+
+

Retrieves GitHub check run as ogr object.

+

Args

+
+
project
+
Project from which the check run is retrieved.
+
check_run_id
+
+

Check run ID.

+

Defaults to None, i.e. is not used for query.

+
+
commit_sha
+
+

Commit SHA from which the check run is to be retrieved. +If set, returns latest check run for the commit.

+

Defaults to None, i.e. is not used for query.

+
+
+

Returns

+

GithubCheckRun object or None if no check run is found.

+

Raises

+

OperationNotSupported, in case there is no parameter for query set +or both are set.

+
+ +Expand source code + +
@staticmethod
+def get(
+    project: "ogr_github.GithubProject",
+    check_run_id: Optional[int] = None,
+    commit_sha: Optional[str] = None,
+) -> Optional["GithubCheckRun"]:
+    """
+    Retrieves GitHub check run as ogr object.
+
+    Args:
+        project: Project from which the check run is retrieved.
+        check_run_id: Check run ID.
+
+            Defaults to `None`, i.e. is not used for query.
+        commit_sha: Commit SHA from which the check run is to be retrieved.
+            If set, returns latest check run for the commit.
+
+            Defaults to `None`, i.e. is not used for query.
+
+    Returns:
+        GithubCheckRun object or `None` if no check run is found.
+
+    Raises:
+        OperationNotSupported, in case there is no parameter for query set
+            or both are set.
+    """
+    if check_run_id is not None and commit_sha:
+        raise OperationNotSupported(
+            "Cannot retrieve check run by both ID and commit hash",
+        )
+
+    if not (check_run_id is not None or commit_sha):
+        raise OperationNotSupported("Cannot retrieve check run by no criteria")
+
+    if check_run_id is not None:
+        return GithubCheckRun(
+            project,
+            project.github_repo.get_check_run(check_run_id),
+        )
+
+    check_runs = project.github_repo.get_commit(commit_sha).get_check_runs()
+    if check_runs.totalCount == 0:
+        return None
+    return GithubCheckRun(project, check_runs[0])
+
+
+
+def get_list(project: ogr_github.GithubProject, commit_sha: str, name: Optional[str] = None, status: Optional[GithubCheckRunStatus] = None) ‑> list['GithubCheckRun'] +
+
+

Returns list of GitHub check runs.

+

Args

+
+
project
+
Project from which the check runs are retrieved.
+
commit_sha
+
Commit to which are the check runs related to.
+
name
+
+

Name of the check run for filtering.

+

Defaults to None, no filtering.

+
+
status
+
+

Status of the check runs to be returned.

+

Defaults to None, no filtering.

+
+
+

Returns

+

List of the check runs.

+
+ +Expand source code + +
@staticmethod
+def get_list(
+    project: "ogr_github.GithubProject",
+    commit_sha: str,
+    name: Optional[str] = None,
+    status: Optional[GithubCheckRunStatus] = None,
+) -> list["GithubCheckRun"]:
+    """
+    Returns list of GitHub check runs.
+
+    Args:
+        project: Project from which the check runs are retrieved.
+        commit_sha: Commit to which are the check runs related to.
+        name: Name of the check run for filtering.
+
+            Defaults to `None`, no filtering.
+        status: Status of the check runs to be returned.
+
+            Defaults to `None`, no filtering.
+
+    Returns:
+        List of the check runs.
+    """
+    check_runs = project.github_repo.get_commit(commit_sha).get_check_runs(
+        check_name=value_or_NotSet(name),
+        status=value_or_NotSet(status.name if status else None),
+    )
+
+    return [GithubCheckRun(project, run) for run in check_runs]
+
+
+
+

Instance variables

+
+
var app : github.GithubApp.GithubApp
+
+

Github App of the check run.

+
+ +Expand source code + +
@property
+def app(self) -> GithubApp:
+    """Github App of the check run."""
+    return self.raw_check_run.app
+
+
+
var commit_sha : str
+
+

Commit SHA that check run is related to.

+
+ +Expand source code + +
@property
+def commit_sha(self) -> str:
+    """Commit SHA that check run is related to."""
+    return self.raw_check_run.head_sha
+
+
+
var completed_at : Optional[datetime.datetime]
+
+

Timestamp of completion of the check run.

+
+ +Expand source code + +
@property
+def completed_at(self) -> Optional[datetime.datetime]:
+    """Timestamp of completion of the check run."""
+    return self.raw_check_run.completed_at
+
+
+
var conclusion : Optional[GithubCheckRunResult]
+
+

Conclusion/result of the check run.

+
+ +Expand source code + +
@property
+def conclusion(self) -> Optional[GithubCheckRunResult]:
+    """Conclusion/result of the check run."""
+    return (
+        GithubCheckRunResult(self.raw_check_run.conclusion)
+        if self.raw_check_run.conclusion
+        else None
+    )
+
+
+
var external_id : Optional[str]
+
+

External ID that can be used internally by the integrator.

+
+ +Expand source code + +
@property
+def external_id(self) -> Optional[str]:
+    """External ID that can be used internally by the integrator."""
+    return self.raw_check_run.external_id
+
+
+
var name : str
+
+

Name of the check run.

+
+ +Expand source code + +
@property
+def name(self) -> str:
+    """Name of the check run."""
+    return self.raw_check_run.name
+
+
+
var output : github.CheckRunOutput.CheckRunOutput
+
+

Output of the check run.

+
+ +Expand source code + +
@property
+def output(self) -> CheckRunOutput:
+    """Output of the check run."""
+    return self.raw_check_run.output
+
+
+
var started_at : Optional[datetime.datetime]
+
+

Timestamp of start of the check run.

+
+ +Expand source code + +
@property
+def started_at(self) -> Optional[datetime.datetime]:
+    """Timestamp of start of the check run."""
+    return self.raw_check_run.started_at
+
+
+
var statusGithubCheckRunStatus
+
+

Current status of the check run.

+
+ +Expand source code + +
@property
+def status(self) -> GithubCheckRunStatus:
+    """Current status of the check run."""
+    return GithubCheckRunStatus(self.raw_check_run.status)
+
+
+
var url : Optional[str]
+
+

URL with additional details.

+
+ +Expand source code + +
@property
+def url(self) -> Optional[str]:
+    """URL with additional details."""
+    return self.raw_check_run.details_url
+
+
+
+

Methods

+
+
+def change_status(self, status: Optional[GithubCheckRunStatus] = None, completed_at: Optional[datetime.datetime] = None, conclusion: Optional[GithubCheckRunResult] = None) ‑> None +
+
+

Changes the status of the check run and checks the validity of new state.

+

Args

+
+
status
+
+

Status of the check run to be set. If set to completed, you +must provide conclusion.

+

Defaults to None.

+
+
completed_at
+
+

Timestamp of completion of the check run. If set, you +must provide conclusion.

+

Defaults to None.

+
+
conclusion
+
+

Conclusion/result of the check run. If only conclusion +is set, status is automatically set to completed.

+

Defaults to None.

+
+
+

Raises

+

OperationNotSupported, if given completed or timestamp of completed +without conclusion.

+
+ +Expand source code + +
def change_status(
+    self,
+    status: Optional[GithubCheckRunStatus] = None,
+    completed_at: Optional[datetime.datetime] = None,
+    conclusion: Optional[GithubCheckRunResult] = None,
+) -> None:
+    """
+    Changes the status of the check run and checks the validity of new state.
+
+    Args:
+        status: Status of the check run to be set. If set to completed, you
+            must provide conclusion.
+
+            Defaults to `None`.
+        completed_at: Timestamp of completion of the check run. If set, you
+            must provide conclusion.
+
+            Defaults to `None`.
+        conclusion: Conclusion/result of the check run. If only conclusion
+            is set, status is automatically set to completed.
+
+            Defaults to `None`.
+
+    Raises:
+        OperationNotSupported, if given completed or timestamp of completed
+            without conclusion.
+    """
+    if not (status or completed_at or conclusion):
+        return
+
+    if (
+        status == GithubCheckRunStatus.completed or completed_at
+    ) and conclusion is None:
+        raise OperationNotSupported(
+            "When provided completed status or completed at,"
+            " you need to provide conclusion.",
+        )
+
+    self.raw_check_run.edit(
+        status=value_or_NotSet(status.name if status else None),
+        conclusion=value_or_NotSet(conclusion.name if conclusion else None),
+        completed_at=value_or_NotSet(completed_at),
+    )
+
+
+
+
+
+class GithubCheckRunResult +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Represents conclusion/result of the GitHub check run.

+
+ +Expand source code + +
class GithubCheckRunResult(Enum):
+    """
+    Represents conclusion/result of the GitHub check run.
+    """
+
+    action_required = "action_required"
+    cancelled = "cancelled"
+    failure = "failure"
+    neutral = "neutral"
+    success = "success"
+    skipped = "skipped"
+    stale = "stale"
+    timed_out = "timed_out"
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var action_required
+
+
+
+
var cancelled
+
+
+
+
var failure
+
+
+
+
var neutral
+
+
+
+
var skipped
+
+
+
+
var stale
+
+
+
+
var success
+
+
+
+
var timed_out
+
+
+
+
+
+
+class GithubCheckRunStatus +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Represents statuses GitHub check run can have.

+
+ +Expand source code + +
class GithubCheckRunStatus(Enum):
+    """
+    Represents statuses GitHub check run can have.
+    """
+
+    queued = "queued"
+    in_progress = "in_progress"
+    completed = "completed"
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var completed
+
+
+
+
var in_progress
+
+
+
+
var queued
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/comments.html b/docs/services/github/comments.html new file mode 100644 index 00000000..86b2ea20 --- /dev/null +++ b/docs/services/github/comments.html @@ -0,0 +1,316 @@ + + + + + + +ogr.services.github.comments API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.comments

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from typing import Union
+
+from github.IssueComment import IssueComment as _GithubIssueComment
+from github.PullRequestComment import PullRequestComment as _GithubPullRequestComment
+from github.Reaction import Reaction as _Reaction
+
+from ogr.abstract import Comment, IssueComment, PRComment, Reaction
+
+
+class GithubReaction(Reaction):
+    _raw_reaction: _Reaction
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    def delete(self) -> None:
+        self._raw_reaction.delete()
+
+
+class GithubComment(Comment):
+    def _from_raw_comment(
+        self,
+        raw_comment: Union[_GithubIssueComment, _GithubPullRequestComment],
+    ) -> None:
+        self._raw_comment = raw_comment
+        self._id = raw_comment.id
+        self._author = raw_comment.user.login
+        self._created = raw_comment.created_at
+
+    @property
+    def body(self) -> str:
+        return self._raw_comment.body
+
+    @body.setter
+    def body(self, new_body: str) -> None:
+        self._raw_comment.edit(new_body)
+
+    @property
+    def edited(self) -> datetime.datetime:
+        return self._raw_comment.updated_at
+
+    def get_reactions(self) -> list[Reaction]:
+        return [
+            GithubReaction(reaction) for reaction in self._raw_comment.get_reactions()
+        ]
+
+    def add_reaction(self, reaction: str) -> GithubReaction:
+        return GithubReaction(self._raw_comment.create_reaction(reaction))
+
+
+class GithubIssueComment(GithubComment, IssueComment):
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+
+class GithubPRComment(GithubComment, PRComment):
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class GithubComment(Comment):
+    def _from_raw_comment(
+        self,
+        raw_comment: Union[_GithubIssueComment, _GithubPullRequestComment],
+    ) -> None:
+        self._raw_comment = raw_comment
+        self._id = raw_comment.id
+        self._author = raw_comment.user.login
+        self._created = raw_comment.created_at
+
+    @property
+    def body(self) -> str:
+        return self._raw_comment.body
+
+    @body.setter
+    def body(self, new_body: str) -> None:
+        self._raw_comment.edit(new_body)
+
+    @property
+    def edited(self) -> datetime.datetime:
+        return self._raw_comment.updated_at
+
+    def get_reactions(self) -> list[Reaction]:
+        return [
+            GithubReaction(reaction) for reaction in self._raw_comment.get_reactions()
+        ]
+
+    def add_reaction(self, reaction: str) -> GithubReaction:
+        return GithubReaction(self._raw_comment.create_reaction(reaction))
+
+

Ancestors

+ +

Subclasses

+ +

Inherited members

+ +
+
+class GithubIssueComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class GithubIssueComment(GithubComment, IssueComment):
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+class GithubPRComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class GithubPRComment(GithubComment, PRComment):
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+class GithubReaction +(raw_reaction: Any) +
+
+
+
+ +Expand source code + +
class GithubReaction(Reaction):
+    _raw_reaction: _Reaction
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    def delete(self) -> None:
+        self._raw_reaction.delete()
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/flag.html b/docs/services/github/flag.html new file mode 100644 index 00000000..7d03c850 --- /dev/null +++ b/docs/services/github/flag.html @@ -0,0 +1,238 @@ + + + + + + +ogr.services.github.flag API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.flag

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from typing import ClassVar
+
+from github import UnknownObjectException
+
+from ogr.abstract import CommitFlag, CommitStatus
+from ogr.services import github as ogr_github
+from ogr.services.base import BaseCommitFlag
+
+
+class GithubCommitFlag(BaseCommitFlag):
+    _states: ClassVar[dict[str, CommitStatus]] = {
+        "pending": CommitStatus.pending,
+        "success": CommitStatus.success,
+        "failure": CommitStatus.failure,
+        "error": CommitStatus.error,
+    }
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    def _from_raw_commit_flag(self):
+        self.state = self._state_from_str(self._raw_commit_flag.state)
+        self.context = self._raw_commit_flag.context
+        self.comment = self._raw_commit_flag.description
+        self.url = self._raw_commit_flag.target_url
+        self.uid = self._raw_commit_flag.id
+
+    @staticmethod
+    def get(project: "ogr_github.GithubProject", commit: str) -> list["CommitFlag"]:
+        statuses = project.github_repo.get_commit(commit).get_statuses()
+
+        try:
+            return [
+                GithubCommitFlag(
+                    raw_commit_flag=raw_status,
+                    project=project,
+                    commit=commit,
+                )
+                for raw_status in statuses
+            ]
+        except UnknownObjectException:
+            return []
+
+    @staticmethod
+    def set(
+        project: "ogr_github.GithubProject",
+        commit: str,
+        state: CommitStatus,
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        state = GithubCommitFlag._validate_state(state)
+
+        github_commit = project.github_repo.get_commit(commit)
+        if trim:
+            description = description[:140]
+        status = github_commit.create_status(
+            state.name,
+            target_url,
+            description,
+            context,
+        )
+        return GithubCommitFlag(project=project, raw_commit_flag=status, commit=commit)
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_commit_flag.created_at
+
+    @property
+    def edited(self) -> datetime.datetime:
+        return self._raw_commit_flag.updated_at
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubCommitFlag +(raw_commit_flag: Optional[Any] = None, project: Optional[ForwardRef('GitProject')] = None, commit: Optional[str] = None, state: Optional[CommitStatus] = None, context: Optional[str] = None, comment: Optional[str] = None, uid: Optional[str] = None, url: Optional[str] = None) +
+
+
+
+ +Expand source code + +
class GithubCommitFlag(BaseCommitFlag):
+    _states: ClassVar[dict[str, CommitStatus]] = {
+        "pending": CommitStatus.pending,
+        "success": CommitStatus.success,
+        "failure": CommitStatus.failure,
+        "error": CommitStatus.error,
+    }
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    def _from_raw_commit_flag(self):
+        self.state = self._state_from_str(self._raw_commit_flag.state)
+        self.context = self._raw_commit_flag.context
+        self.comment = self._raw_commit_flag.description
+        self.url = self._raw_commit_flag.target_url
+        self.uid = self._raw_commit_flag.id
+
+    @staticmethod
+    def get(project: "ogr_github.GithubProject", commit: str) -> list["CommitFlag"]:
+        statuses = project.github_repo.get_commit(commit).get_statuses()
+
+        try:
+            return [
+                GithubCommitFlag(
+                    raw_commit_flag=raw_status,
+                    project=project,
+                    commit=commit,
+                )
+                for raw_status in statuses
+            ]
+        except UnknownObjectException:
+            return []
+
+    @staticmethod
+    def set(
+        project: "ogr_github.GithubProject",
+        commit: str,
+        state: CommitStatus,
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        state = GithubCommitFlag._validate_state(state)
+
+        github_commit = project.github_repo.get_commit(commit)
+        if trim:
+            description = description[:140]
+        status = github_commit.create_status(
+            state.name,
+            target_url,
+            description,
+            context,
+        )
+        return GithubCommitFlag(project=project, raw_commit_flag=status, commit=commit)
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_commit_flag.created_at
+
+    @property
+    def edited(self) -> datetime.datetime:
+        return self._raw_commit_flag.updated_at
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/index.html b/docs/services/github/index.html new file mode 100644 index 00000000..875cbb6e --- /dev/null +++ b/docs/services/github/index.html @@ -0,0 +1,3141 @@ + + + + + + +ogr.services.github API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from ogr.services.github.check_run import GithubCheckRun
+from ogr.services.github.comments import GithubIssueComment, GithubPRComment
+from ogr.services.github.issue import GithubIssue
+from ogr.services.github.project import GithubProject
+from ogr.services.github.pull_request import GithubPullRequest
+from ogr.services.github.release import GithubRelease
+from ogr.services.github.service import GithubService
+from ogr.services.github.user import GithubUser
+
+__all__ = [
+    GithubCheckRun.__name__,
+    GithubPullRequest.__name__,
+    GithubIssueComment.__name__,
+    GithubPRComment.__name__,
+    GithubIssue.__name__,
+    GithubRelease.__name__,
+    GithubUser.__name__,
+    GithubProject.__name__,
+    GithubService.__name__,
+]
+
+
+
+

Sub-modules

+
+
ogr.services.github.auth_providers
+
+
+
+
ogr.services.github.check_run
+
+
+
+
ogr.services.github.comments
+
+
+
+
ogr.services.github.flag
+
+
+
+
ogr.services.github.issue
+
+
+
+
ogr.services.github.project
+
+
+
+
ogr.services.github.pull_request
+
+
+
+
ogr.services.github.release
+
+
+
+
ogr.services.github.service
+
+
+
+
ogr.services.github.user
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubCheckRun +(project: ogr_github.GithubProject, raw_check_run: github.CheckRun.CheckRun) +
+
+
+
+ +Expand source code + +
class GithubCheckRun(OgrAbstractClass):
+    def __init__(
+        self,
+        project: "ogr_github.GithubProject",
+        raw_check_run: CheckRun,
+    ) -> None:
+        self.raw_check_run = raw_check_run
+        self.project = project
+
+    def __str__(self) -> str:
+        return (
+            f"GithubCheckRun(project={self.project}, name='{self.name}', "
+            f"commit_sha='{self.commit_sha}', "
+            f"url='{self.url}', "
+            f"external_id='{self.external_id}', "
+            f"status={self.status.name}, "
+            f"started_at={self.started_at}, "
+            f"conclusion={self.conclusion}, "
+            f"completed_at={self.completed_at}, "
+            f"output={self.output}, "
+            f"app={self.app})"
+        )
+
+    @property
+    def name(self) -> str:
+        """Name of the check run."""
+        return self.raw_check_run.name
+
+    @name.setter
+    def name(self, name: str) -> None:
+        self.raw_check_run.edit(name=name)
+
+    @property
+    def commit_sha(self) -> str:
+        """Commit SHA that check run is related to."""
+        return self.raw_check_run.head_sha
+
+    @property
+    def url(self) -> Optional[str]:
+        """URL with additional details."""
+        return self.raw_check_run.details_url
+
+    @url.setter
+    def url(self, url: str) -> None:
+        self.raw_check_run.edit(details_url=url)
+
+    @property
+    def external_id(self) -> Optional[str]:
+        """External ID that can be used internally by the integrator."""
+        return self.raw_check_run.external_id
+
+    @external_id.setter
+    def external_id(self, external_id: str) -> None:
+        self.raw_check_run.edit(external_id=external_id)
+
+    @property
+    def status(self) -> GithubCheckRunStatus:
+        """Current status of the check run."""
+        return GithubCheckRunStatus(self.raw_check_run.status)
+
+    @property
+    def started_at(self) -> Optional[datetime.datetime]:
+        """Timestamp of start of the check run."""
+        return self.raw_check_run.started_at
+
+    @started_at.setter
+    def started_at(self, started_at: datetime.datetime) -> None:
+        self.raw_check_run.edit(started_at=started_at)
+
+    @property
+    def conclusion(self) -> Optional[GithubCheckRunResult]:
+        """Conclusion/result of the check run."""
+        return (
+            GithubCheckRunResult(self.raw_check_run.conclusion)
+            if self.raw_check_run.conclusion
+            else None
+        )
+
+    @property
+    def completed_at(self) -> Optional[datetime.datetime]:
+        """Timestamp of completion of the check run."""
+        return self.raw_check_run.completed_at
+
+    @property
+    def output(self) -> CheckRunOutput:
+        """Output of the check run."""
+        return self.raw_check_run.output
+
+    @output.setter
+    def output(self, output: GithubCheckRunOutput) -> None:
+        self.raw_check_run.edit(output=output)
+
+    @property
+    def app(self) -> GithubApp:
+        """Github App of the check run."""
+        return self.raw_check_run.app
+
+    def change_status(
+        self,
+        status: Optional[GithubCheckRunStatus] = None,
+        completed_at: Optional[datetime.datetime] = None,
+        conclusion: Optional[GithubCheckRunResult] = None,
+    ) -> None:
+        """
+        Changes the status of the check run and checks the validity of new state.
+
+        Args:
+            status: Status of the check run to be set. If set to completed, you
+                must provide conclusion.
+
+                Defaults to `None`.
+            completed_at: Timestamp of completion of the check run. If set, you
+                must provide conclusion.
+
+                Defaults to `None`.
+            conclusion: Conclusion/result of the check run. If only conclusion
+                is set, status is automatically set to completed.
+
+                Defaults to `None`.
+
+        Raises:
+            OperationNotSupported, if given completed or timestamp of completed
+                without conclusion.
+        """
+        if not (status or completed_at or conclusion):
+            return
+
+        if (
+            status == GithubCheckRunStatus.completed or completed_at
+        ) and conclusion is None:
+            raise OperationNotSupported(
+                "When provided completed status or completed at,"
+                " you need to provide conclusion.",
+            )
+
+        self.raw_check_run.edit(
+            status=value_or_NotSet(status.name if status else None),
+            conclusion=value_or_NotSet(conclusion.name if conclusion else None),
+            completed_at=value_or_NotSet(completed_at),
+        )
+
+    @staticmethod
+    def get_list(
+        project: "ogr_github.GithubProject",
+        commit_sha: str,
+        name: Optional[str] = None,
+        status: Optional[GithubCheckRunStatus] = None,
+    ) -> list["GithubCheckRun"]:
+        """
+        Returns list of GitHub check runs.
+
+        Args:
+            project: Project from which the check runs are retrieved.
+            commit_sha: Commit to which are the check runs related to.
+            name: Name of the check run for filtering.
+
+                Defaults to `None`, no filtering.
+            status: Status of the check runs to be returned.
+
+                Defaults to `None`, no filtering.
+
+        Returns:
+            List of the check runs.
+        """
+        check_runs = project.github_repo.get_commit(commit_sha).get_check_runs(
+            check_name=value_or_NotSet(name),
+            status=value_or_NotSet(status.name if status else None),
+        )
+
+        return [GithubCheckRun(project, run) for run in check_runs]
+
+    @staticmethod
+    def get(
+        project: "ogr_github.GithubProject",
+        check_run_id: Optional[int] = None,
+        commit_sha: Optional[str] = None,
+    ) -> Optional["GithubCheckRun"]:
+        """
+        Retrieves GitHub check run as ogr object.
+
+        Args:
+            project: Project from which the check run is retrieved.
+            check_run_id: Check run ID.
+
+                Defaults to `None`, i.e. is not used for query.
+            commit_sha: Commit SHA from which the check run is to be retrieved.
+                If set, returns latest check run for the commit.
+
+                Defaults to `None`, i.e. is not used for query.
+
+        Returns:
+            GithubCheckRun object or `None` if no check run is found.
+
+        Raises:
+            OperationNotSupported, in case there is no parameter for query set
+                or both are set.
+        """
+        if check_run_id is not None and commit_sha:
+            raise OperationNotSupported(
+                "Cannot retrieve check run by both ID and commit hash",
+            )
+
+        if not (check_run_id is not None or commit_sha):
+            raise OperationNotSupported("Cannot retrieve check run by no criteria")
+
+        if check_run_id is not None:
+            return GithubCheckRun(
+                project,
+                project.github_repo.get_check_run(check_run_id),
+            )
+
+        check_runs = project.github_repo.get_commit(commit_sha).get_check_runs()
+        if check_runs.totalCount == 0:
+            return None
+        return GithubCheckRun(project, check_runs[0])
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        name: str,
+        commit_sha: str,
+        url: Optional[str] = None,
+        external_id: Optional[str] = None,
+        status: GithubCheckRunStatus = GithubCheckRunStatus.queued,
+        started_at: Optional[datetime.datetime] = None,
+        conclusion: Optional[GithubCheckRunResult] = None,
+        completed_at: Optional[datetime.datetime] = None,
+        output: Optional[GithubCheckRunOutput] = None,
+        actions: Optional[list[dict[str, str]]] = None,
+    ) -> "GithubCheckRun":
+        """
+        Creates new check run.
+
+        Args:
+            project: Project where the check run is to be created.
+            name: Name of the check run.
+            commit_sha: Hash of the commit that check run is related to.
+            url: URL with details of the run.
+
+                Defaults to `None`.
+            external_id: External ID that can be used internally by integrator.
+
+                Defaults to `None`.
+            status: Status of the check run.
+
+                Defaults to queued.
+            started_at: Timestamp of starting the check run.
+
+                Defaults to `None`.
+            conclusion: Conclusion of the check run. Should be set with status
+                completed.
+
+                Defaults to `None`.
+            completed_at: Timestamp of completion of the check run. If set, you
+                must provide conclusion.
+
+                Defaults to `None`.
+            output: Output of the check run.
+            actions: List of possible follow-up actions for the check run.
+
+        Returns:
+            Created check run object.
+
+        Raises:
+            OperationNotSupported, if given completed status or completion
+                timestamp and no conclusion.
+        """
+
+        if (
+            completed_at or status == GithubCheckRunStatus.completed
+        ) and conclusion is None:
+            raise OperationNotSupported(
+                "When provided completed_at or completed status, "
+                "you need to provide conclusion.",
+            )
+
+        created_check_run = project.github_repo.create_check_run(
+            name=name,
+            head_sha=commit_sha,
+            details_url=value_or_NotSet(url),
+            external_id=value_or_NotSet(external_id),
+            status=status.name,
+            started_at=value_or_NotSet(started_at),
+            conclusion=value_or_NotSet(conclusion.name if conclusion else None),
+            completed_at=value_or_NotSet(completed_at),
+            output=value_or_NotSet(output),
+            actions=value_or_NotSet(actions),
+        )
+
+        return GithubCheckRun(project, created_check_run)
+
+

Ancestors

+ +

Static methods

+
+
+def create(project: ogr_github.GithubProject, name: str, commit_sha: str, url: Optional[str] = None, external_id: Optional[str] = None, status: GithubCheckRunStatus = GithubCheckRunStatus.queued, started_at: Optional[datetime.datetime] = None, conclusion: Optional[GithubCheckRunResult] = None, completed_at: Optional[datetime.datetime] = None, output: Optional[dict[str, typing.Union[str, list[dict[str, typing.Union[str, int]]]]]] = None, actions: Optional[list[dict[str, str]]] = None) ‑> GithubCheckRun +
+
+

Creates new check run.

+

Args

+
+
project
+
Project where the check run is to be created.
+
name
+
Name of the check run.
+
commit_sha
+
Hash of the commit that check run is related to.
+
url
+
+

URL with details of the run.

+

Defaults to None.

+
+
external_id
+
+

External ID that can be used internally by integrator.

+

Defaults to None.

+
+
status
+
+

Status of the check run.

+

Defaults to queued.

+
+
started_at
+
+

Timestamp of starting the check run.

+

Defaults to None.

+
+
conclusion
+
+

Conclusion of the check run. Should be set with status +completed.

+

Defaults to None.

+
+
completed_at
+
+

Timestamp of completion of the check run. If set, you +must provide conclusion.

+

Defaults to None.

+
+
output
+
Output of the check run.
+
actions
+
List of possible follow-up actions for the check run.
+
+

Returns

+

Created check run object.

+

Raises

+

OperationNotSupported, if given completed status or completion +timestamp and no conclusion.

+
+ +Expand source code + +
@staticmethod
+def create(
+    project: "ogr_github.GithubProject",
+    name: str,
+    commit_sha: str,
+    url: Optional[str] = None,
+    external_id: Optional[str] = None,
+    status: GithubCheckRunStatus = GithubCheckRunStatus.queued,
+    started_at: Optional[datetime.datetime] = None,
+    conclusion: Optional[GithubCheckRunResult] = None,
+    completed_at: Optional[datetime.datetime] = None,
+    output: Optional[GithubCheckRunOutput] = None,
+    actions: Optional[list[dict[str, str]]] = None,
+) -> "GithubCheckRun":
+    """
+    Creates new check run.
+
+    Args:
+        project: Project where the check run is to be created.
+        name: Name of the check run.
+        commit_sha: Hash of the commit that check run is related to.
+        url: URL with details of the run.
+
+            Defaults to `None`.
+        external_id: External ID that can be used internally by integrator.
+
+            Defaults to `None`.
+        status: Status of the check run.
+
+            Defaults to queued.
+        started_at: Timestamp of starting the check run.
+
+            Defaults to `None`.
+        conclusion: Conclusion of the check run. Should be set with status
+            completed.
+
+            Defaults to `None`.
+        completed_at: Timestamp of completion of the check run. If set, you
+            must provide conclusion.
+
+            Defaults to `None`.
+        output: Output of the check run.
+        actions: List of possible follow-up actions for the check run.
+
+    Returns:
+        Created check run object.
+
+    Raises:
+        OperationNotSupported, if given completed status or completion
+            timestamp and no conclusion.
+    """
+
+    if (
+        completed_at or status == GithubCheckRunStatus.completed
+    ) and conclusion is None:
+        raise OperationNotSupported(
+            "When provided completed_at or completed status, "
+            "you need to provide conclusion.",
+        )
+
+    created_check_run = project.github_repo.create_check_run(
+        name=name,
+        head_sha=commit_sha,
+        details_url=value_or_NotSet(url),
+        external_id=value_or_NotSet(external_id),
+        status=status.name,
+        started_at=value_or_NotSet(started_at),
+        conclusion=value_or_NotSet(conclusion.name if conclusion else None),
+        completed_at=value_or_NotSet(completed_at),
+        output=value_or_NotSet(output),
+        actions=value_or_NotSet(actions),
+    )
+
+    return GithubCheckRun(project, created_check_run)
+
+
+
+def get(project: ogr_github.GithubProject, check_run_id: Optional[int] = None, commit_sha: Optional[str] = None) ‑> Optional[GithubCheckRun] +
+
+

Retrieves GitHub check run as ogr object.

+

Args

+
+
project
+
Project from which the check run is retrieved.
+
check_run_id
+
+

Check run ID.

+

Defaults to None, i.e. is not used for query.

+
+
commit_sha
+
+

Commit SHA from which the check run is to be retrieved. +If set, returns latest check run for the commit.

+

Defaults to None, i.e. is not used for query.

+
+
+

Returns

+

GithubCheckRun object or None if no check run is found.

+

Raises

+

OperationNotSupported, in case there is no parameter for query set +or both are set.

+
+ +Expand source code + +
@staticmethod
+def get(
+    project: "ogr_github.GithubProject",
+    check_run_id: Optional[int] = None,
+    commit_sha: Optional[str] = None,
+) -> Optional["GithubCheckRun"]:
+    """
+    Retrieves GitHub check run as ogr object.
+
+    Args:
+        project: Project from which the check run is retrieved.
+        check_run_id: Check run ID.
+
+            Defaults to `None`, i.e. is not used for query.
+        commit_sha: Commit SHA from which the check run is to be retrieved.
+            If set, returns latest check run for the commit.
+
+            Defaults to `None`, i.e. is not used for query.
+
+    Returns:
+        GithubCheckRun object or `None` if no check run is found.
+
+    Raises:
+        OperationNotSupported, in case there is no parameter for query set
+            or both are set.
+    """
+    if check_run_id is not None and commit_sha:
+        raise OperationNotSupported(
+            "Cannot retrieve check run by both ID and commit hash",
+        )
+
+    if not (check_run_id is not None or commit_sha):
+        raise OperationNotSupported("Cannot retrieve check run by no criteria")
+
+    if check_run_id is not None:
+        return GithubCheckRun(
+            project,
+            project.github_repo.get_check_run(check_run_id),
+        )
+
+    check_runs = project.github_repo.get_commit(commit_sha).get_check_runs()
+    if check_runs.totalCount == 0:
+        return None
+    return GithubCheckRun(project, check_runs[0])
+
+
+
+def get_list(project: ogr_github.GithubProject, commit_sha: str, name: Optional[str] = None, status: Optional[GithubCheckRunStatus] = None) ‑> list['GithubCheckRun'] +
+
+

Returns list of GitHub check runs.

+

Args

+
+
project
+
Project from which the check runs are retrieved.
+
commit_sha
+
Commit to which are the check runs related to.
+
name
+
+

Name of the check run for filtering.

+

Defaults to None, no filtering.

+
+
status
+
+

Status of the check runs to be returned.

+

Defaults to None, no filtering.

+
+
+

Returns

+

List of the check runs.

+
+ +Expand source code + +
@staticmethod
+def get_list(
+    project: "ogr_github.GithubProject",
+    commit_sha: str,
+    name: Optional[str] = None,
+    status: Optional[GithubCheckRunStatus] = None,
+) -> list["GithubCheckRun"]:
+    """
+    Returns list of GitHub check runs.
+
+    Args:
+        project: Project from which the check runs are retrieved.
+        commit_sha: Commit to which are the check runs related to.
+        name: Name of the check run for filtering.
+
+            Defaults to `None`, no filtering.
+        status: Status of the check runs to be returned.
+
+            Defaults to `None`, no filtering.
+
+    Returns:
+        List of the check runs.
+    """
+    check_runs = project.github_repo.get_commit(commit_sha).get_check_runs(
+        check_name=value_or_NotSet(name),
+        status=value_or_NotSet(status.name if status else None),
+    )
+
+    return [GithubCheckRun(project, run) for run in check_runs]
+
+
+
+

Instance variables

+
+
var app : github.GithubApp.GithubApp
+
+

Github App of the check run.

+
+ +Expand source code + +
@property
+def app(self) -> GithubApp:
+    """Github App of the check run."""
+    return self.raw_check_run.app
+
+
+
var commit_sha : str
+
+

Commit SHA that check run is related to.

+
+ +Expand source code + +
@property
+def commit_sha(self) -> str:
+    """Commit SHA that check run is related to."""
+    return self.raw_check_run.head_sha
+
+
+
var completed_at : Optional[datetime.datetime]
+
+

Timestamp of completion of the check run.

+
+ +Expand source code + +
@property
+def completed_at(self) -> Optional[datetime.datetime]:
+    """Timestamp of completion of the check run."""
+    return self.raw_check_run.completed_at
+
+
+
var conclusion : Optional[GithubCheckRunResult]
+
+

Conclusion/result of the check run.

+
+ +Expand source code + +
@property
+def conclusion(self) -> Optional[GithubCheckRunResult]:
+    """Conclusion/result of the check run."""
+    return (
+        GithubCheckRunResult(self.raw_check_run.conclusion)
+        if self.raw_check_run.conclusion
+        else None
+    )
+
+
+
var external_id : Optional[str]
+
+

External ID that can be used internally by the integrator.

+
+ +Expand source code + +
@property
+def external_id(self) -> Optional[str]:
+    """External ID that can be used internally by the integrator."""
+    return self.raw_check_run.external_id
+
+
+
var name : str
+
+

Name of the check run.

+
+ +Expand source code + +
@property
+def name(self) -> str:
+    """Name of the check run."""
+    return self.raw_check_run.name
+
+
+
var output : github.CheckRunOutput.CheckRunOutput
+
+

Output of the check run.

+
+ +Expand source code + +
@property
+def output(self) -> CheckRunOutput:
+    """Output of the check run."""
+    return self.raw_check_run.output
+
+
+
var started_at : Optional[datetime.datetime]
+
+

Timestamp of start of the check run.

+
+ +Expand source code + +
@property
+def started_at(self) -> Optional[datetime.datetime]:
+    """Timestamp of start of the check run."""
+    return self.raw_check_run.started_at
+
+
+
var statusGithubCheckRunStatus
+
+

Current status of the check run.

+
+ +Expand source code + +
@property
+def status(self) -> GithubCheckRunStatus:
+    """Current status of the check run."""
+    return GithubCheckRunStatus(self.raw_check_run.status)
+
+
+
var url : Optional[str]
+
+

URL with additional details.

+
+ +Expand source code + +
@property
+def url(self) -> Optional[str]:
+    """URL with additional details."""
+    return self.raw_check_run.details_url
+
+
+
+

Methods

+
+
+def change_status(self, status: Optional[GithubCheckRunStatus] = None, completed_at: Optional[datetime.datetime] = None, conclusion: Optional[GithubCheckRunResult] = None) ‑> None +
+
+

Changes the status of the check run and checks the validity of new state.

+

Args

+
+
status
+
+

Status of the check run to be set. If set to completed, you +must provide conclusion.

+

Defaults to None.

+
+
completed_at
+
+

Timestamp of completion of the check run. If set, you +must provide conclusion.

+

Defaults to None.

+
+
conclusion
+
+

Conclusion/result of the check run. If only conclusion +is set, status is automatically set to completed.

+

Defaults to None.

+
+
+

Raises

+

OperationNotSupported, if given completed or timestamp of completed +without conclusion.

+
+ +Expand source code + +
def change_status(
+    self,
+    status: Optional[GithubCheckRunStatus] = None,
+    completed_at: Optional[datetime.datetime] = None,
+    conclusion: Optional[GithubCheckRunResult] = None,
+) -> None:
+    """
+    Changes the status of the check run and checks the validity of new state.
+
+    Args:
+        status: Status of the check run to be set. If set to completed, you
+            must provide conclusion.
+
+            Defaults to `None`.
+        completed_at: Timestamp of completion of the check run. If set, you
+            must provide conclusion.
+
+            Defaults to `None`.
+        conclusion: Conclusion/result of the check run. If only conclusion
+            is set, status is automatically set to completed.
+
+            Defaults to `None`.
+
+    Raises:
+        OperationNotSupported, if given completed or timestamp of completed
+            without conclusion.
+    """
+    if not (status or completed_at or conclusion):
+        return
+
+    if (
+        status == GithubCheckRunStatus.completed or completed_at
+    ) and conclusion is None:
+        raise OperationNotSupported(
+            "When provided completed status or completed at,"
+            " you need to provide conclusion.",
+        )
+
+    self.raw_check_run.edit(
+        status=value_or_NotSet(status.name if status else None),
+        conclusion=value_or_NotSet(conclusion.name if conclusion else None),
+        completed_at=value_or_NotSet(completed_at),
+    )
+
+
+
+
+
+class GithubIssue +(raw_issue: github.Issue.Issue, project: ogr_github.GithubProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the issue.
+
+
+ +Expand source code + +
class GithubIssue(BaseIssue):
+    raw_issue: _GithubIssue
+
+    def __init__(
+        self,
+        raw_issue: _GithubIssue,
+        project: "ogr_github.GithubProject",
+    ) -> None:
+        if raw_issue.pull_request:
+            raise GithubAPIException(
+                f"Requested issue #{raw_issue.number} is a pull request",
+            )
+
+        super().__init__(raw_issue=raw_issue, project=project)
+
+    @property
+    def title(self) -> str:
+        return self._raw_issue.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_issue.edit(title=new_title)
+
+    @property
+    def id(self) -> int:
+        return self._raw_issue.number
+
+    @property
+    def status(self) -> IssueStatus:
+        return IssueStatus[self._raw_issue.state]
+
+    @property
+    def url(self) -> str:
+        return self._raw_issue.html_url
+
+    @property
+    def assignees(self) -> list:
+        return self._raw_issue.assignees
+
+    @property
+    def description(self) -> str:
+        return self._raw_issue.body
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_issue.edit(body=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_issue.user.login
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_issue.created_at
+
+    @property
+    def labels(self) -> list:
+        return list(self._raw_issue.get_labels())
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list] = None,
+    ) -> "Issue":
+        if private:
+            raise OperationNotSupported("Private issues are not supported by Github")
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        github_issue = project.github_repo.create_issue(
+            title=title,
+            body=body,
+            labels=labels or [],
+            assignees=assignees or [],
+        )
+        return GithubIssue(github_issue, project)
+
+    @staticmethod
+    def get(project: "ogr_github.GithubProject", issue_id: int) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        try:
+            issue = project.github_repo.get_issue(number=issue_id)
+        except github.UnknownObjectException as ex:
+            raise GithubAPIException(f"No issue with id {issue_id} found") from ex
+        return GithubIssue(issue, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_github.GithubProject",
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        parameters: dict[str, Union[str, list[str]]] = {
+            "state": status.name,
+            "sort": "updated",
+            "direction": "desc",
+        }
+        if author:
+            parameters["creator"] = author
+        if assignee:
+            parameters["assignee"] = assignee
+        if labels:
+            parameters["labels"] = [
+                project.github_repo.get_label(label) for label in labels
+            ]
+
+        issues = project.github_repo.get_issues(**parameters)
+        try:
+            return [
+                GithubIssue(issue, project)
+                for issue in issues
+                if not issue.pull_request
+            ]
+        except UnknownObjectException:
+            return []
+
+    def _get_all_comments(self) -> list[IssueComment]:
+        return [
+            GithubIssueComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_issue.get_comments()
+        ]
+
+    def comment(self, body: str) -> IssueComment:
+        comment = self._raw_issue.create_comment(body)
+        return GithubIssueComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "Issue":
+        self._raw_issue.edit(state="closed")
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        for label in labels:
+            self._raw_issue.add_to_labels(label)
+
+    def add_assignee(self, *assignees: str) -> None:
+        try:
+            self._raw_issue.edit(assignees=list(assignees))
+        except github.GithubException as ex:
+            raise GithubAPIException("Failed to assign issue, unknown user") from ex
+
+    def get_comment(self, comment_id: int) -> IssueComment:
+        return GithubIssueComment(self._raw_issue.get_comment(comment_id))
+
+

Ancestors

+ +

Class variables

+
+
var raw_issue : github.Issue.Issue
+
+
+
+
+

Instance variables

+
+
var assignees : list
+
+
+
+ +Expand source code + +
@property
+def assignees(self) -> list:
+    return self._raw_issue.assignees
+
+
+
+

Inherited members

+ +
+
+class GithubIssueComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class GithubIssueComment(GithubComment, IssueComment):
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+class GithubPRComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class GithubPRComment(GithubComment, PRComment):
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+class GithubProject +(repo: str, service: ogr_github.GithubService, namespace: str, github_repo: github.Repository.Repository = None, read_only: bool = False, **unprocess_kwargs) +
+
+

Args

+
+
repo
+
Name of the project.
+
service
+
GitService instance.
+
namespace
+
+

Namespace of the project.

+
    +
  • GitHub: username or org name.
  • +
  • GitLab: username or org name.
  • +
  • Pagure: namespace (e.g. "rpms").
  • +
+

In case of forks: "fork/{username}/{namespace}".

+
+
+
+ +Expand source code + +
class GithubProject(BaseGitProject):
+    service: "ogr_github.GithubService"
+    # Permission levels that can merge PRs
+    CAN_MERGE_PERMS: ClassVar[set[str]] = {"admin", "write"}
+
+    def __init__(
+        self,
+        repo: str,
+        service: "ogr_github.GithubService",
+        namespace: str,
+        github_repo: Repository = None,
+        read_only: bool = False,
+        **unprocess_kwargs,
+    ) -> None:
+        if unprocess_kwargs:
+            logger.warning(
+                f"GithubProject will not process these kwargs: {unprocess_kwargs}",
+            )
+        super().__init__(repo, service, namespace)
+        self._github_repo = github_repo
+        self.read_only = read_only
+
+        self._github_instance = None
+
+    @property
+    def github_instance(self):
+        if not self._github_instance:
+            self._github_instance = self.service.get_pygithub_instance(
+                self.namespace,
+                self.repo,
+            )
+
+        return self._github_instance
+
+    @property
+    def github_repo(self):
+        if not self._github_repo:
+            self._github_repo = self.github_instance.get_repo(
+                full_name_or_id=f"{self.namespace}/{self.repo}",
+            )
+        return self._github_repo
+
+    def __str__(self) -> str:
+        return f'GithubProject(namespace="{self.namespace}", repo="{self.repo}")'
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, GithubProject):
+            return False
+
+        return (
+            self.repo == o.repo
+            and self.namespace == o.namespace
+            and self.service == o.service
+            and self.read_only == o.read_only
+        )
+
+    @property
+    def description(self) -> str:
+        return self.github_repo.description
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.github_repo.edit(description=new_description)
+
+    @property
+    def has_issues(self) -> bool:
+        return self.github_repo.has_issues
+
+    def _construct_fork_project(self) -> Optional["GithubProject"]:
+        gh_user = self.github_instance.get_user()
+        user_login = gh_user.login
+        try:
+            project = GithubProject(
+                self.repo,
+                self.service,
+                namespace=user_login,
+                read_only=self.read_only,
+            )
+            if not project.github_repo:
+                # The github_repo attribute is lazy.
+                return None
+            return project
+        except github.GithubException as ex:
+            logger.debug(f"Project {user_login}/{self.repo} does not exist: {ex}")
+            return None
+
+    def exists(self) -> bool:
+        try:
+            _ = self.github_repo
+            return True
+        except UnknownObjectException as ex:
+            if "Not Found" in str(ex):
+                return False
+            raise GithubAPIException from ex
+
+    def is_private(self) -> bool:
+        return self.github_repo.private
+
+    def is_forked(self) -> bool:
+        return bool(self._construct_fork_project())
+
+    @property
+    def is_fork(self) -> bool:
+        return self.github_repo.fork
+
+    @property
+    def parent(self) -> Optional["GithubProject"]:
+        return (
+            self.service.get_project_from_github_repository(self.github_repo.parent)
+            if self.is_fork
+            else None
+        )
+
+    @property
+    def default_branch(self):
+        return self.github_repo.default_branch
+
+    def get_branches(self) -> list[str]:
+        return [branch.name for branch in self.github_repo.get_branches()]
+
+    def get_description(self) -> str:
+        return self.github_repo.description
+
+    def add_user(self, user: str, access_level: AccessLevel) -> None:
+        access_dict = {
+            AccessLevel.pull: "Pull",
+            AccessLevel.triage: "Triage",
+            AccessLevel.push: "Push",
+            AccessLevel.admin: "Admin",
+            AccessLevel.maintain: "Maintain",
+        }
+        try:
+            invitation = self.github_repo.add_to_collaborators(
+                user,
+                permission=access_dict[access_level],
+            )
+        except Exception as ex:
+            raise GithubAPIException(f"User {user} not found") from ex
+
+        if invitation is None:
+            raise GithubAPIException("User already added")
+
+    def request_access(self):
+        raise OperationNotSupported("Not possible on GitHub")
+
+    def get_fork(self, create: bool = True) -> Optional["GithubProject"]:
+        username = self.service.user.get_username()
+        for fork in self.get_forks():
+            if fork.github_repo.owner.login == username:
+                return fork
+
+        if not self.is_forked():
+            if create:
+                return self.fork_create()
+
+            logger.info(
+                f"Fork of {self.github_repo.full_name}"
+                " does not exist and we were asked not to create it.",
+            )
+            return None
+        return self._construct_fork_project()
+
+    def get_owners(self) -> list[str]:
+        # in case of github, repository has only one owner
+        return [self.github_repo.owner.login]
+
+    def __get_collaborators(self) -> set[str]:
+        try:
+            collaborators = self._get_collaborators_with_permission()
+        except github.GithubException:
+            logger.debug(
+                "Current Github token must have push access to view repository permissions.",
+            )
+            return set()
+
+        usernames = []
+        for login, permission in collaborators.items():
+            if permission in self.CAN_MERGE_PERMS:
+                usernames.append(login)
+
+        return set(usernames)
+
+    def who_can_close_issue(self) -> set[str]:
+        return self.__get_collaborators()
+
+    def who_can_merge_pr(self) -> set[str]:
+        return self.__get_collaborators()
+
+    def can_merge_pr(self, username) -> bool:
+        return (
+            self.github_repo.get_collaborator_permission(username)
+            in self.CAN_MERGE_PERMS
+        )
+
+    def _get_collaborators_with_permission(self) -> dict:
+        """
+        Get all project collaborators in dictionary with permission association.
+
+        Returns:
+            Dictionary with logins of collaborators and their permission level.
+        """
+        collaborators = {}
+        users = self.github_repo.get_collaborators()
+        for user in users:
+            permission = self.github_repo.get_collaborator_permission(user)
+            collaborators[user.login] = permission
+        return collaborators
+
+    @indirect(GithubIssue.get_list)
+    def get_issue_list(
+        self,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list[Issue]:
+        pass
+
+    @indirect(GithubIssue.get)
+    def get_issue(self, issue_id: int) -> Issue:
+        pass
+
+    @indirect(GithubIssue.create)
+    def create_issue(
+        self,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> Issue:
+        pass
+
+    def delete(self) -> None:
+        self.github_repo.delete()
+
+    @indirect(GithubPullRequest.get_list)
+    def get_pr_list(self, status: PRStatus = PRStatus.open) -> list[PullRequest]:
+        pass
+
+    @indirect(GithubPullRequest.get)
+    def get_pr(self, pr_id: int) -> PullRequest:
+        pass
+
+    def get_sha_from_tag(self, tag_name: str) -> str:
+        # TODO: This is ugly. Can we do it better?
+        all_tags = self.github_repo.get_tags()
+        for tag in all_tags:
+            if tag.name == tag_name:
+                return tag.commit.sha
+        raise GithubAPIException(f"Tag {tag_name} was not found.")
+
+    def get_tag_from_tag_name(self, tag_name: str) -> Optional[GitTag]:
+        """
+        Get a tag based on a tag name.
+
+        Args:
+            tag_name: Name of the tag.
+
+        Returns:
+            GitTag associated with the given tag name or `None`.
+        """
+        all_tags = self.github_repo.get_tags()
+        for tag in all_tags:
+            if tag.name == tag_name:
+                return GitTag(name=tag.name, commit_sha=tag.commit.sha)
+        return None
+
+    @if_readonly(return_function=GitProjectReadOnly.create_pr)
+    @indirect(GithubPullRequest.create)
+    def create_pr(
+        self,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> PullRequest:
+        pass
+
+    @if_readonly(
+        return_function=GitProjectReadOnly.commit_comment,
+        log_message="Create Comment to commit",
+    )
+    def commit_comment(
+        self,
+        commit: str,
+        body: str,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> CommitComment:
+        github_commit: Commit = self.github_repo.get_commit(commit)
+        if filename and row:
+            comment = github_commit.create_comment(
+                body=body,
+                position=row,
+                path=filename,
+            )
+        else:
+            comment = github_commit.create_comment(body=body)
+        return self._commit_comment_from_github_object(comment)
+
+    @staticmethod
+    def _commit_comment_from_github_object(
+        raw_commit_coment: GithubCommitComment,
+    ) -> CommitComment:
+        return CommitComment(
+            body=raw_commit_coment.body,
+            author=raw_commit_coment.user.login,
+            sha=raw_commit_coment.commit_id,
+        )
+
+    def get_commit_comments(self, commit: str) -> list[CommitComment]:
+        github_commit: Commit = self.github_repo.get_commit(commit)
+        return [
+            self._commit_comment_from_github_object(comment)
+            for comment in github_commit.get_comments()
+        ]
+
+    @if_readonly(
+        return_function=GitProjectReadOnly.set_commit_status,
+        log_message="Create a status on a commit",
+    )
+    @indirect(GithubCommitFlag.set)
+    def set_commit_status(
+        self,
+        commit: str,
+        state: Union[CommitStatus, str],
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ):
+        pass
+
+    @indirect(GithubCommitFlag.get)
+    def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+        pass
+
+    @indirect(GithubCheckRun.get)
+    def get_check_run(
+        self,
+        check_run_id: Optional[int] = None,
+        commit_sha: Optional[str] = None,
+    ) -> Optional["GithubCheckRun"]:
+        pass
+
+    @indirect(GithubCheckRun.create)
+    def create_check_run(
+        self,
+        name: str,
+        commit_sha: str,
+        url: Optional[str] = None,
+        external_id: Optional[str] = None,
+        status: GithubCheckRunStatus = GithubCheckRunStatus.queued,
+        started_at: Optional[datetime.datetime] = None,
+        conclusion: Optional[GithubCheckRunResult] = None,
+        completed_at: Optional[datetime.datetime] = None,
+        output: Optional[GithubCheckRunOutput] = None,
+        actions: Optional[list[dict[str, str]]] = None,
+    ) -> "GithubCheckRun":
+        pass
+
+    @indirect(GithubCheckRun.get_list)
+    def get_check_runs(
+        self,
+        commit_sha: str,
+        name: Optional[str] = None,
+        status: Optional[GithubCheckRunStatus] = None,
+    ) -> list["GithubCheckRun"]:
+        pass
+
+    def get_git_urls(self) -> dict[str, str]:
+        return {"git": self.github_repo.clone_url, "ssh": self.github_repo.ssh_url}
+
+    @if_readonly(return_function=GitProjectReadOnly.fork_create)
+    def fork_create(self, namespace: Optional[str] = None) -> "GithubProject":
+        fork_repo = (
+            self.github_repo.create_fork(organization=namespace)
+            if namespace
+            else self.github_repo.create_fork()
+        )
+
+        fork = self.service.get_project_from_github_repository(fork_repo)
+        logger.debug(f"Forked to {fork.namespace}/{fork.repo}")
+        return fork
+
+    def change_token(self, new_token: str):
+        raise OperationNotSupported
+
+    def get_file_content(self, path: str, ref=None) -> str:
+        ref = ref or self.default_branch
+        try:
+            return self.github_repo.get_contents(
+                path=path,
+                ref=ref,
+            ).decoded_content.decode()
+        except (UnknownObjectException, GithubException) as ex:
+            if ex.status == 404:
+                raise FileNotFoundError(f"File '{path}' on {ref} not found") from ex
+            raise GithubAPIException() from ex
+
+    def get_files(
+        self,
+        ref: Optional[str] = None,
+        filter_regex: Optional[str] = None,
+        recursive: bool = False,
+    ) -> list[str]:
+        ref = ref or self.default_branch
+        paths = []
+        contents = self.github_repo.get_contents(path="", ref=ref)
+
+        if recursive:
+            while contents:
+                file_content = contents.pop(0)
+                if file_content.type == "dir":
+                    contents.extend(
+                        self.github_repo.get_contents(path=file_content.path, ref=ref),
+                    )
+                else:
+                    paths.append(file_content.path)
+
+        else:
+            paths = [
+                file_content.path
+                for file_content in contents
+                if file_content.type != "dir"
+            ]
+
+        if filter_regex:
+            paths = filter_paths(paths, filter_regex)
+
+        return paths
+
+    def get_labels(self):
+        """
+        Get list of labels in the repository.
+
+        Returns:
+            List of labels in the repository.
+        """
+        return list(self.github_repo.get_labels())
+
+    def update_labels(self, labels):
+        """
+        Update the labels of the repository. (No deletion, only add not existing ones.)
+
+        Args:
+            labels: List of labels to be added.
+
+        Returns:
+            Number of added labels.
+        """
+        current_label_names = [la.name for la in list(self.github_repo.get_labels())]
+        changes = 0
+        for label in labels:
+            if label.name not in current_label_names:
+                color = self._normalize_label_color(color=label.color)
+                self.github_repo.create_label(
+                    name=label.name,
+                    color=color,
+                    description=label.description or "",
+                )
+
+                changes += 1
+        return changes
+
+    @staticmethod
+    def _normalize_label_color(color):
+        if color.startswith("#"):
+            return color[1:]
+        return color
+
+    @indirect(GithubRelease.get)
+    def get_release(self, identifier=None, name=None, tag_name=None) -> GithubRelease:
+        pass
+
+    @indirect(GithubRelease.get_latest)
+    def get_latest_release(self) -> Optional[GithubRelease]:
+        pass
+
+    @indirect(GithubRelease.get_list)
+    def get_releases(self) -> list[Release]:
+        pass
+
+    @indirect(GithubRelease.create)
+    def create_release(self, tag: str, name: str, message: str) -> GithubRelease:
+        pass
+
+    def get_forks(self) -> list["GithubProject"]:
+        return [
+            self.service.get_project_from_github_repository(fork)
+            for fork in self.github_repo.get_forks()
+            if fork.owner
+        ]
+
+    def get_web_url(self) -> str:
+        return self.github_repo.html_url
+
+    def get_tags(self) -> list["GitTag"]:
+        return [GitTag(tag.name, tag.commit.sha) for tag in self.github_repo.get_tags()]
+
+    def get_sha_from_branch(self, branch: str) -> Optional[str]:
+        try:
+            return self.github_repo.get_branch(branch).commit.sha
+        except GithubException as ex:
+            if ex.status == 404:
+                return None
+            raise GithubAPIException from ex
+
+    def get_contributors(self) -> set[str]:
+        """
+        Returns:
+            Logins of contributors to the project.
+        """
+        return {c.login for c in self.github_repo.get_contributors()}
+
+    def users_with_write_access(self) -> set[str]:
+        return self.__get_collaborators()
+
+

Ancestors

+ +

Class variables

+
+
var CAN_MERGE_PERMS : ClassVar[set[str]]
+
+
+
+
var serviceGithubService
+
+
+
+
+

Instance variables

+
+
var github_instance
+
+
+
+ +Expand source code + +
@property
+def github_instance(self):
+    if not self._github_instance:
+        self._github_instance = self.service.get_pygithub_instance(
+            self.namespace,
+            self.repo,
+        )
+
+    return self._github_instance
+
+
+
var github_repo
+
+
+
+ +Expand source code + +
@property
+def github_repo(self):
+    if not self._github_repo:
+        self._github_repo = self.github_instance.get_repo(
+            full_name_or_id=f"{self.namespace}/{self.repo}",
+        )
+    return self._github_repo
+
+
+
+

Methods

+
+
+def create_check_run(self, name: str, commit_sha: str, url: Optional[str] = None, external_id: Optional[str] = None, status: GithubCheckRunStatus = GithubCheckRunStatus.queued, started_at: Optional[datetime.datetime] = None, conclusion: Optional[GithubCheckRunResult] = None, completed_at: Optional[datetime.datetime] = None, output: Optional[dict[str, typing.Union[str, list[dict[str, typing.Union[str, int]]]]]] = None, actions: Optional[list[dict[str, str]]] = None) ‑> GithubCheckRun +
+
+
+
+ +Expand source code + +
@indirect(GithubCheckRun.create)
+def create_check_run(
+    self,
+    name: str,
+    commit_sha: str,
+    url: Optional[str] = None,
+    external_id: Optional[str] = None,
+    status: GithubCheckRunStatus = GithubCheckRunStatus.queued,
+    started_at: Optional[datetime.datetime] = None,
+    conclusion: Optional[GithubCheckRunResult] = None,
+    completed_at: Optional[datetime.datetime] = None,
+    output: Optional[GithubCheckRunOutput] = None,
+    actions: Optional[list[dict[str, str]]] = None,
+) -> "GithubCheckRun":
+    pass
+
+
+
+def get_check_run(self, check_run_id: Optional[int] = None, commit_sha: Optional[str] = None) ‑> Optional[GithubCheckRun] +
+
+
+
+ +Expand source code + +
@indirect(GithubCheckRun.get)
+def get_check_run(
+    self,
+    check_run_id: Optional[int] = None,
+    commit_sha: Optional[str] = None,
+) -> Optional["GithubCheckRun"]:
+    pass
+
+
+
+def get_check_runs(self, commit_sha: str, name: Optional[str] = None, status: Optional[GithubCheckRunStatus] = None) ‑> list['GithubCheckRun'] +
+
+
+
+ +Expand source code + +
@indirect(GithubCheckRun.get_list)
+def get_check_runs(
+    self,
+    commit_sha: str,
+    name: Optional[str] = None,
+    status: Optional[GithubCheckRunStatus] = None,
+) -> list["GithubCheckRun"]:
+    pass
+
+
+
+def get_contributors(self) ‑> set[str] +
+
+

Returns

+

Logins of contributors to the project.

+
+ +Expand source code + +
def get_contributors(self) -> set[str]:
+    """
+    Returns:
+        Logins of contributors to the project.
+    """
+    return {c.login for c in self.github_repo.get_contributors()}
+
+
+
+def get_labels(self) +
+
+

Get list of labels in the repository.

+

Returns

+

List of labels in the repository.

+
+ +Expand source code + +
def get_labels(self):
+    """
+    Get list of labels in the repository.
+
+    Returns:
+        List of labels in the repository.
+    """
+    return list(self.github_repo.get_labels())
+
+
+
+def get_tag_from_tag_name(self, tag_name: str) ‑> Optional[GitTag] +
+
+

Get a tag based on a tag name.

+

Args

+
+
tag_name
+
Name of the tag.
+
+

Returns

+

GitTag associated with the given tag name or None.

+
+ +Expand source code + +
def get_tag_from_tag_name(self, tag_name: str) -> Optional[GitTag]:
+    """
+    Get a tag based on a tag name.
+
+    Args:
+        tag_name: Name of the tag.
+
+    Returns:
+        GitTag associated with the given tag name or `None`.
+    """
+    all_tags = self.github_repo.get_tags()
+    for tag in all_tags:
+        if tag.name == tag_name:
+            return GitTag(name=tag.name, commit_sha=tag.commit.sha)
+    return None
+
+
+
+def update_labels(self, labels) +
+
+

Update the labels of the repository. (No deletion, only add not existing ones.)

+

Args

+
+
labels
+
List of labels to be added.
+
+

Returns

+

Number of added labels.

+
+ +Expand source code + +
def update_labels(self, labels):
+    """
+    Update the labels of the repository. (No deletion, only add not existing ones.)
+
+    Args:
+        labels: List of labels to be added.
+
+    Returns:
+        Number of added labels.
+    """
+    current_label_names = [la.name for la in list(self.github_repo.get_labels())]
+    changes = 0
+    for label in labels:
+        if label.name not in current_label_names:
+            color = self._normalize_label_color(color=label.color)
+            self.github_repo.create_label(
+                name=label.name,
+                color=color,
+                description=label.description or "",
+            )
+
+            changes += 1
+    return changes
+
+
+
+

Inherited members

+ +
+
+class GithubPullRequest +(raw_pr: Any, project: GitProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the pull request.
+
+
+ +Expand source code + +
class GithubPullRequest(BasePullRequest):
+    _raw_pr: _GithubPullRequest
+    _target_project: "ogr_github.GithubProject"
+    _source_project: "ogr_github.GithubProject" = None
+
+    @property
+    def title(self) -> str:
+        return self._raw_pr.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_pr.edit(title=new_title)
+
+    @property
+    def id(self) -> int:
+        return self._raw_pr.number
+
+    @property
+    def status(self) -> PRStatus:
+        return (
+            PRStatus.merged
+            if self._raw_pr.is_merged()
+            else PRStatus[self._raw_pr.state]
+        )
+
+    @property
+    def url(self) -> str:
+        return self._raw_pr.html_url
+
+    @property
+    def description(self) -> str:
+        return self._raw_pr.body
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_pr.edit(body=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_pr.user.login
+
+    @property
+    def source_branch(self) -> str:
+        return self._raw_pr.head.ref
+
+    @property
+    def target_branch(self) -> str:
+        return self._raw_pr.base.ref
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_pr.created_at
+
+    @property
+    def labels(self) -> list[GithubLabel]:
+        return list(self._raw_pr.get_labels())
+
+    @property
+    def diff_url(self) -> str:
+        return f"{self._raw_pr.html_url}/files"
+
+    @property
+    def patch(self) -> bytes:
+        response = requests.get(self._raw_pr.patch_url)
+
+        if not response.ok:
+            cls = OgrNetworkError if response.status_code >= 500 else GithubAPIException
+            raise cls(
+                f"Couldn't get patch from {self._raw_pr.patch_url} because {response.reason}.",
+            )
+
+        return response.content
+
+    @property
+    def commits_url(self) -> str:
+        return f"{self._raw_pr.html_url}/commits"
+
+    @property
+    def head_commit(self) -> str:
+        return self._raw_pr.head.sha
+
+    @property
+    def merge_commit_sha(self) -> str:
+        return self._raw_pr.merge_commit_sha
+
+    @property
+    def merge_commit_status(self) -> MergeCommitStatus:
+        if self._raw_pr.mergeable:
+            return MergeCommitStatus.can_be_merged
+
+        return MergeCommitStatus.cannot_be_merged
+
+    @property
+    def source_project(self) -> "ogr_github.GithubProject":
+        if self._source_project is None:
+            self._source_project = (
+                self._target_project.service.get_project_from_github_repository(
+                    self._raw_pr.head.repo,
+                )
+            )
+
+        return self._source_project
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        The default behavior is the pull request is made to the immediate parent repository
+        if the repository is a forked repository.
+        If you want to create a pull request to the forked repo, please pass
+        the `fork_username` parameter.
+        """
+        github_repo = project.github_repo
+
+        target_project = project
+        if project.is_fork and fork_username is None:
+            logger.warning(f"{project.full_repo_name} is fork, ignoring fork_repo.")
+            source_branch = f"{project.namespace}:{source_branch}"
+            github_repo = project.parent.github_repo
+            target_project = project.parent
+        elif fork_username:
+            source_branch = f"{fork_username}:{source_branch}"
+            if fork_username != project.namespace and project.parent is not None:
+                github_repo = GithubPullRequest.__get_fork(
+                    fork_username,
+                    project.parent.github_repo,
+                )
+
+        created_pr = github_repo.create_pull(
+            title=title,
+            body=body,
+            base=target_branch,
+            head=source_branch,
+        )
+        logger.info(f"PR {created_pr.id} created: {target_branch}<-{source_branch}")
+        return GithubPullRequest(created_pr, target_project)
+
+    @staticmethod
+    def __get_fork(fork_username: str, repo: _GithubRepository) -> _GithubRepository:
+        forks = list(
+            filter(lambda fork: fork.owner.login == fork_username, repo.get_forks()),
+        )
+        if not forks:
+            raise GithubAPIException("Requested fork doesn't exist")
+        return forks[0]
+
+    @staticmethod
+    def get(project: "ogr_github.GithubProject", pr_id: int) -> "PullRequest":
+        try:
+            pr = project.github_repo.get_pull(number=pr_id)
+        except github.UnknownObjectException as ex:
+            raise GithubAPIException(f"No pull request with id {pr_id} found") from ex
+        return GithubPullRequest(pr, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_github.GithubProject",
+        status: PRStatus = PRStatus.open,
+    ) -> list["PullRequest"]:
+        prs = project.github_repo.get_pulls(
+            # Github API has no status 'merged', just 'closed'/'opened'/'all'
+            state=status.name if status != PRStatus.merged else "closed",
+            sort="updated",
+            direction="desc",
+        )
+
+        if status == PRStatus.merged:
+            prs = list(prs)  # Github PaginatedList into list()
+            for pr in prs:
+                if not pr.is_merged():  # parse merged PRs
+                    prs.remove(pr)
+        try:
+            return [GithubPullRequest(pr, project) for pr in prs]
+        except UnknownObjectException:
+            return []
+
+    def update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "PullRequest":
+        try:
+            self._raw_pr.edit(title=title, body=description)
+            logger.info(f"PR updated: {self._raw_pr.url}")
+            return self
+        except Exception as ex:
+            raise GithubAPIException("there was an error while updating the PR") from ex
+
+    def _get_all_comments(self) -> list[PRComment]:
+        return [
+            GithubPRComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_pr.get_issue_comments()
+        ]
+
+    def get_all_commits(self) -> list[str]:
+        return [commit.sha for commit in self._raw_pr.get_commits()]
+
+    def comment(
+        self,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        comment: Union[_GithubIssueComment, _GithubPullRequestComment] = None
+        if not any([commit, filename, row]):
+            comment = self._raw_pr.create_issue_comment(body)
+        else:
+            github_commit = self._target_project.github_repo.get_commit(commit)
+            comment = self._raw_pr.create_comment(body, github_commit, filename, row)
+        return GithubPRComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "PullRequest":
+        self._raw_pr.edit(state=PRStatus.closed.name)
+        return self
+
+    def merge(self) -> "PullRequest":
+        self._raw_pr.merge()
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        for label in labels:
+            self._raw_pr.add_to_labels(label)
+
+    def get_comment(self, comment_id: int) -> PRComment:
+        return GithubPRComment(self._raw_pr.get_issue_comment(comment_id))
+
+

Ancestors

+ +

Static methods

+
+
+def create(project: ogr_github.GithubProject, title: str, body: str, target_branch: str, source_branch: str, fork_username: Optional[str] = None) ‑> PullRequest +
+
+

The default behavior is the pull request is made to the immediate parent repository +if the repository is a forked repository. +If you want to create a pull request to the forked repo, please pass +the fork_username parameter.

+
+ +Expand source code + +
@staticmethod
+def create(
+    project: "ogr_github.GithubProject",
+    title: str,
+    body: str,
+    target_branch: str,
+    source_branch: str,
+    fork_username: Optional[str] = None,
+) -> "PullRequest":
+    """
+    The default behavior is the pull request is made to the immediate parent repository
+    if the repository is a forked repository.
+    If you want to create a pull request to the forked repo, please pass
+    the `fork_username` parameter.
+    """
+    github_repo = project.github_repo
+
+    target_project = project
+    if project.is_fork and fork_username is None:
+        logger.warning(f"{project.full_repo_name} is fork, ignoring fork_repo.")
+        source_branch = f"{project.namespace}:{source_branch}"
+        github_repo = project.parent.github_repo
+        target_project = project.parent
+    elif fork_username:
+        source_branch = f"{fork_username}:{source_branch}"
+        if fork_username != project.namespace and project.parent is not None:
+            github_repo = GithubPullRequest.__get_fork(
+                fork_username,
+                project.parent.github_repo,
+            )
+
+    created_pr = github_repo.create_pull(
+        title=title,
+        body=body,
+        base=target_branch,
+        head=source_branch,
+    )
+    logger.info(f"PR {created_pr.id} created: {target_branch}<-{source_branch}")
+    return GithubPullRequest(created_pr, target_project)
+
+
+
+

Inherited members

+ +
+
+class GithubRelease +(raw_release: Any, project: GitProject) +
+
+

Object that represents release.

+

Attributes

+
+
project : GitProject
+
Project on which the release is created.
+
+
+ +Expand source code + +
class GithubRelease(Release):
+    _raw_release: PyGithubRelease
+    project: "ogr_github.GithubProject"
+
+    @staticmethod
+    def _release_id_from_name(
+        project: "ogr_github.GithubProject",
+        name: str,
+    ) -> Optional[int]:
+        releases = project.github_repo.get_releases()
+        for release in releases:
+            if release.title == name:
+                return release.id
+        return None
+
+    @staticmethod
+    def _release_id_from_tag(
+        project: "ogr_github.GithubProject",
+        tag: str,
+    ) -> Optional[int]:
+        releases = project.github_repo.get_releases()
+        for release in releases:
+            if release.tag_name == tag:
+                return release.id
+        return None
+
+    @property
+    def title(self):
+        return self._raw_release.title
+
+    @property
+    def body(self):
+        return self._raw_release.body
+
+    @property
+    def git_tag(self) -> GitTag:
+        return self.project.get_tag_from_tag_name(self.tag_name)
+
+    @property
+    def tag_name(self) -> str:
+        return self._raw_release.tag_name
+
+    @property
+    def url(self) -> Optional[str]:
+        return self._raw_release.html_url
+
+    @property
+    def created_at(self) -> datetime.datetime:
+        return self._raw_release.created_at
+
+    @property
+    def tarball_url(self) -> str:
+        return self._raw_release.tarball_url
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    @staticmethod
+    def get(
+        project: "ogr_github.GithubProject",
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> "Release":
+        if tag_name:
+            identifier = GithubRelease._release_id_from_tag(project, tag_name)
+        elif name:
+            identifier = GithubRelease._release_id_from_name(project, name)
+        if identifier is None:
+            raise GithubAPIException("Release was not found.")
+        release = project.github_repo.get_release(id=identifier)
+        return GithubRelease(release, project)
+
+    @staticmethod
+    def get_latest(project: "ogr_github.GithubProject") -> Optional["Release"]:
+        try:
+            release = project.github_repo.get_latest_release()
+            return GithubRelease(release, project)
+        except GithubException as ex:
+            if ex.status == 404:
+                return None
+            raise GithubAPIException from ex
+
+    @staticmethod
+    def get_list(project: "ogr_github.GithubProject") -> list["Release"]:
+        releases = project.github_repo.get_releases()
+        return [GithubRelease(release, project) for release in releases]
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> "Release":
+        created_release = project.github_repo.create_git_release(
+            tag=tag,
+            name=name,
+            message=message,
+        )
+        return GithubRelease(created_release, project)
+
+    def edit_release(self, name: str, message: str) -> None:
+        """
+        Edit name and message of a release.
+
+        Args:
+            name: New name of the release.
+            message: New message for the release.
+        """
+        self._raw_release = self._raw_release.update_release(name=name, message=message)
+
+

Ancestors

+ +

Class variables

+
+
var projectGithubProject
+
+
+
+
+

Methods

+
+
+def edit_release(self, name: str, message: str) ‑> None +
+
+

Edit name and message of a release.

+

Args

+
+
name
+
New name of the release.
+
message
+
New message for the release.
+
+
+ +Expand source code + +
def edit_release(self, name: str, message: str) -> None:
+    """
+    Edit name and message of a release.
+
+    Args:
+        name: New name of the release.
+        message: New message for the release.
+    """
+    self._raw_release = self._raw_release.update_release(name=name, message=message)
+
+
+
+

Inherited members

+ +
+
+class GithubService +(token=None, read_only=False, github_app_id: Optional[str] = None, github_app_private_key: Optional[str] = None, github_app_private_key_path: Optional[str] = None, tokman_instance_url: Optional[str] = None, github_authentication: GithubAuthentication = None, max_retries: Union[int, urllib3.util.retry.Retry] = 0, **kwargs) +
+
+

Attributes

+
+
instance_url : str
+
URL of the git forge instance.
+
+

If multiple authentication methods are provided, they are prioritised: +1. Tokman +2. GithubApp +3. TokenAuthentication (which is also default one, that works without specified token)

+
+ +Expand source code + +
@use_for_service("github.com")
+class GithubService(BaseGitService):
+    # class parameter could be used to mock Github class api
+    github_class: type[github.Github]
+    instance_url = "https://github.com"
+
+    def __init__(
+        self,
+        token=None,
+        read_only=False,
+        github_app_id: Optional[str] = None,
+        github_app_private_key: Optional[str] = None,
+        github_app_private_key_path: Optional[str] = None,
+        tokman_instance_url: Optional[str] = None,
+        github_authentication: GithubAuthentication = None,
+        max_retries: Union[int, Retry] = 0,
+        **kwargs,
+    ):
+        """
+        If multiple authentication methods are provided, they are prioritised:
+            1. Tokman
+            2. GithubApp
+            3. TokenAuthentication (which is also default one, that works without specified token)
+        """
+        super().__init__()
+        self.read_only = read_only
+        self._default_auth_method = github_authentication
+        self._other_auth_method: GithubAuthentication = None
+        self._auth_methods: dict[AuthMethod, GithubAuthentication] = {}
+
+        if isinstance(max_retries, Retry):
+            self._max_retries = max_retries
+        else:
+            self._max_retries = Retry(
+                total=int(max_retries),
+                read=0,
+                # Retry mechanism active for these HTTP methods:
+                allowed_methods=["DELETE", "GET", "PATCH", "POST", "PUT"],
+                # Only retry on following HTTP status codes
+                status_forcelist=[500, 503, 403, 401],
+                raise_on_status=False,
+            )
+
+        if not self._default_auth_method:
+            self.__set_authentication(
+                token=token,
+                github_app_id=github_app_id,
+                github_app_private_key=github_app_private_key,
+                github_app_private_key_path=github_app_private_key_path,
+                tokman_instance_url=tokman_instance_url,
+                max_retries=self._max_retries,
+            )
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    def __set_authentication(self, **kwargs):
+        auth_methods = [
+            (Tokman, AuthMethod.tokman),
+            (GithubApp, AuthMethod.github_app),
+            (TokenAuthentication, AuthMethod.token),
+        ]
+        for auth_class, auth_name in auth_methods:
+            auth_inst = auth_class.try_create(**kwargs)
+            self._auth_methods[auth_name] = auth_inst
+            if not self._default_auth_method:
+                self._default_auth_method = auth_inst
+
+        return None if self._default_auth_method else TokenAuthentication(None)
+
+    def set_auth_method(self, method: AuthMethod):
+        if self._auth_methods[method]:
+            logger.info("Forced Github auth method to %s", method)
+            self._other_auth_method = self._auth_methods[method]
+        else:
+            raise GithubAPIException(
+                f"Choosen authentication method ({method}) is not available",
+            )
+
+    def reset_auth_method(self):
+        logger.info("Reset Github auth method to the default")
+        self._other_auth_method = None
+
+    @property
+    def authentication(self):
+        return self._other_auth_method or self._default_auth_method
+
+    @property
+    def github(self):
+        return self.authentication.pygithub_instance
+
+    def __str__(self) -> str:
+        readonly_str = ", read_only=True" if self.read_only else ""
+        arguments = f", github_authentication={self.authentication!s}{readonly_str}"
+
+        if arguments:
+            # remove the first '- '
+            arguments = arguments[2:]
+
+        return f"GithubService({arguments})"
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, GithubService):
+            return False
+
+        return (
+            self.read_only == o.read_only  # type: ignore
+            and self.authentication == o.authentication  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(
+        self,
+        repo=None,
+        namespace=None,
+        is_fork=False,
+        **kwargs,
+    ) -> "GithubProject":
+        if is_fork:
+            namespace = self.user.get_username()
+        return GithubProject(
+            repo=repo,
+            namespace=namespace,
+            service=self,
+            read_only=self.read_only,
+            **kwargs,
+        )
+
+    def get_project_from_github_repository(
+        self,
+        github_repo: PyGithubRepository.Repository,
+    ) -> "GithubProject":
+        return GithubProject(
+            repo=github_repo.name,
+            namespace=github_repo.owner.login,
+            github_repo=github_repo,
+            service=self,
+            read_only=self.read_only,
+        )
+
+    @property
+    def user(self) -> GitUser:
+        return GithubUser(service=self)
+
+    def change_token(self, new_token: str) -> None:
+        self._default_auth_method = TokenAuthentication(new_token)
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "GithubProject":
+        if namespace:
+            try:
+                owner = self.github.get_organization(namespace)
+            except UnknownObjectException as ex:
+                raise GithubAPIException(f"Group {namespace} not found.") from ex
+        else:
+            owner = self.github.get_user()
+
+        try:
+            new_repo = owner.create_repo(
+                name=repo,
+                description=description if description else github.GithubObject.NotSet,
+            )
+        except github.GithubException as ex:
+            raise GithubAPIException("Project creation failed") from ex
+        return GithubProject(
+            repo=repo,
+            namespace=namespace or owner.login,
+            service=self,
+            github_repo=new_repo,
+        )
+
+    def get_pygithub_instance(self, namespace: str, repo: str) -> PyGithubInstance:
+        token = self.authentication.get_token(namespace, repo)
+        return PyGithubInstance(login_or_token=token, retry=self._max_retries)
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        search_query = ""
+
+        if user:
+            search_query += f"user:{user}"
+
+        if language:
+            search_query += f" language:{language}"
+
+        projects: list[GitProject]
+        projects = [
+            GithubProject(
+                repo=repo.name,
+                namespace=repo.owner.login,
+                github_repo=repo,
+                service=self,
+            )
+            for repo in self.github.search_repositories(search_query, order="asc")
+        ]
+
+        if search_pattern:
+            projects = [
+                project
+                for project in projects
+                if re.search(search_pattern, project.repo)
+            ]
+
+        return projects
+
+

Ancestors

+ +

Class variables

+
+
var github_class : type[github.MainClass.Github]
+
+
+
+
var instance_url : Optional[str]
+
+
+
+
+

Instance variables

+
+
var authentication
+
+
+
+ +Expand source code + +
@property
+def authentication(self):
+    return self._other_auth_method or self._default_auth_method
+
+
+
var github
+
+
+
+ +Expand source code + +
@property
+def github(self):
+    return self.authentication.pygithub_instance
+
+
+
+

Methods

+
+
+def get_project_from_github_repository(self, github_repo: github.Repository.Repository) ‑> GithubProject +
+
+
+
+ +Expand source code + +
def get_project_from_github_repository(
+    self,
+    github_repo: PyGithubRepository.Repository,
+) -> "GithubProject":
+    return GithubProject(
+        repo=github_repo.name,
+        namespace=github_repo.owner.login,
+        github_repo=github_repo,
+        service=self,
+        read_only=self.read_only,
+    )
+
+
+
+def get_pygithub_instance(self, namespace: str, repo: str) ‑> github.MainClass.Github +
+
+
+
+ +Expand source code + +
def get_pygithub_instance(self, namespace: str, repo: str) -> PyGithubInstance:
+    token = self.authentication.get_token(namespace, repo)
+    return PyGithubInstance(login_or_token=token, retry=self._max_retries)
+
+
+
+

Inherited members

+ +
+
+class GithubUser +(service: ogr_github.GithubService) +
+
+

Represents currently authenticated user through service.

+
+ +Expand source code + +
class GithubUser(BaseGitUser):
+    service: "ogr_github.GithubService"
+
+    def __init__(self, service: "ogr_github.GithubService") -> None:
+        super().__init__(service=service)
+
+    def __str__(self) -> str:
+        return f'GithubUser(username="{self.get_username()}")'
+
+    @property
+    def _github_user(self):
+        return self.service.github.get_user()
+
+    def get_username(self) -> str:
+        return self.service.github.get_user().login
+
+    def get_email(self) -> Optional[str]:
+        user_email_property = self.service.github.get_user().email
+        if user_email_property:
+            return user_email_property
+
+        user_emails = self.service.github.get_user().get_emails()
+
+        if not user_emails:
+            return None
+
+        # To work around the braking change introduced by pygithub==1.55
+        # https://pygithub.readthedocs.io/en/latest/changes.html#version-1-55-april-26-2021
+        if isinstance(user_emails[0], dict):
+            EmailData = namedtuple("EmailData", user_emails[0].keys())  # type: ignore
+        for email in user_emails:
+            if "EmailData" in locals():
+                email = EmailData(**email)  # type: ignore
+            if email.primary:
+                return email.email
+
+        # Return the first email we received
+        return user_emails[0]["email"]
+
+    def get_projects(self) -> list["ogr_github.GithubProject"]:
+        raw_repos = self._github_user.get_repos(affiliation="owner")
+        return [
+            GithubProject(
+                repo=repo.name,
+                namespace=repo.owner.login,
+                github_repo=repo,
+                service=self.service,
+            )
+            for repo in raw_repos
+        ]
+
+    def get_forks(self) -> list["ogr_github.GithubProject"]:
+        return [project for project in self.get_projects() if project.github_repo.fork]
+
+

Ancestors

+ +

Class variables

+
+
var serviceGithubService
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/issue.html b/docs/services/github/issue.html new file mode 100644 index 00000000..0d92310e --- /dev/null +++ b/docs/services/github/issue.html @@ -0,0 +1,479 @@ + + + + + + +ogr.services.github.issue API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.issue

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from typing import Optional, Union
+
+import github
+from github import UnknownObjectException
+from github.Issue import Issue as _GithubIssue
+
+from ogr.abstract import Issue, IssueComment, IssueStatus
+from ogr.exceptions import (
+    GithubAPIException,
+    IssueTrackerDisabled,
+    OperationNotSupported,
+)
+from ogr.services import github as ogr_github
+from ogr.services.base import BaseIssue
+from ogr.services.github.comments import GithubIssueComment
+
+
+class GithubIssue(BaseIssue):
+    raw_issue: _GithubIssue
+
+    def __init__(
+        self,
+        raw_issue: _GithubIssue,
+        project: "ogr_github.GithubProject",
+    ) -> None:
+        if raw_issue.pull_request:
+            raise GithubAPIException(
+                f"Requested issue #{raw_issue.number} is a pull request",
+            )
+
+        super().__init__(raw_issue=raw_issue, project=project)
+
+    @property
+    def title(self) -> str:
+        return self._raw_issue.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_issue.edit(title=new_title)
+
+    @property
+    def id(self) -> int:
+        return self._raw_issue.number
+
+    @property
+    def status(self) -> IssueStatus:
+        return IssueStatus[self._raw_issue.state]
+
+    @property
+    def url(self) -> str:
+        return self._raw_issue.html_url
+
+    @property
+    def assignees(self) -> list:
+        return self._raw_issue.assignees
+
+    @property
+    def description(self) -> str:
+        return self._raw_issue.body
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_issue.edit(body=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_issue.user.login
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_issue.created_at
+
+    @property
+    def labels(self) -> list:
+        return list(self._raw_issue.get_labels())
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list] = None,
+    ) -> "Issue":
+        if private:
+            raise OperationNotSupported("Private issues are not supported by Github")
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        github_issue = project.github_repo.create_issue(
+            title=title,
+            body=body,
+            labels=labels or [],
+            assignees=assignees or [],
+        )
+        return GithubIssue(github_issue, project)
+
+    @staticmethod
+    def get(project: "ogr_github.GithubProject", issue_id: int) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        try:
+            issue = project.github_repo.get_issue(number=issue_id)
+        except github.UnknownObjectException as ex:
+            raise GithubAPIException(f"No issue with id {issue_id} found") from ex
+        return GithubIssue(issue, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_github.GithubProject",
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        parameters: dict[str, Union[str, list[str]]] = {
+            "state": status.name,
+            "sort": "updated",
+            "direction": "desc",
+        }
+        if author:
+            parameters["creator"] = author
+        if assignee:
+            parameters["assignee"] = assignee
+        if labels:
+            parameters["labels"] = [
+                project.github_repo.get_label(label) for label in labels
+            ]
+
+        issues = project.github_repo.get_issues(**parameters)
+        try:
+            return [
+                GithubIssue(issue, project)
+                for issue in issues
+                if not issue.pull_request
+            ]
+        except UnknownObjectException:
+            return []
+
+    def _get_all_comments(self) -> list[IssueComment]:
+        return [
+            GithubIssueComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_issue.get_comments()
+        ]
+
+    def comment(self, body: str) -> IssueComment:
+        comment = self._raw_issue.create_comment(body)
+        return GithubIssueComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "Issue":
+        self._raw_issue.edit(state="closed")
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        for label in labels:
+            self._raw_issue.add_to_labels(label)
+
+    def add_assignee(self, *assignees: str) -> None:
+        try:
+            self._raw_issue.edit(assignees=list(assignees))
+        except github.GithubException as ex:
+            raise GithubAPIException("Failed to assign issue, unknown user") from ex
+
+    def get_comment(self, comment_id: int) -> IssueComment:
+        return GithubIssueComment(self._raw_issue.get_comment(comment_id))
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubIssue +(raw_issue: github.Issue.Issue, project: ogr_github.GithubProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the issue.
+
+
+ +Expand source code + +
class GithubIssue(BaseIssue):
+    raw_issue: _GithubIssue
+
+    def __init__(
+        self,
+        raw_issue: _GithubIssue,
+        project: "ogr_github.GithubProject",
+    ) -> None:
+        if raw_issue.pull_request:
+            raise GithubAPIException(
+                f"Requested issue #{raw_issue.number} is a pull request",
+            )
+
+        super().__init__(raw_issue=raw_issue, project=project)
+
+    @property
+    def title(self) -> str:
+        return self._raw_issue.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_issue.edit(title=new_title)
+
+    @property
+    def id(self) -> int:
+        return self._raw_issue.number
+
+    @property
+    def status(self) -> IssueStatus:
+        return IssueStatus[self._raw_issue.state]
+
+    @property
+    def url(self) -> str:
+        return self._raw_issue.html_url
+
+    @property
+    def assignees(self) -> list:
+        return self._raw_issue.assignees
+
+    @property
+    def description(self) -> str:
+        return self._raw_issue.body
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_issue.edit(body=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_issue.user.login
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_issue.created_at
+
+    @property
+    def labels(self) -> list:
+        return list(self._raw_issue.get_labels())
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list] = None,
+    ) -> "Issue":
+        if private:
+            raise OperationNotSupported("Private issues are not supported by Github")
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        github_issue = project.github_repo.create_issue(
+            title=title,
+            body=body,
+            labels=labels or [],
+            assignees=assignees or [],
+        )
+        return GithubIssue(github_issue, project)
+
+    @staticmethod
+    def get(project: "ogr_github.GithubProject", issue_id: int) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        try:
+            issue = project.github_repo.get_issue(number=issue_id)
+        except github.UnknownObjectException as ex:
+            raise GithubAPIException(f"No issue with id {issue_id} found") from ex
+        return GithubIssue(issue, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_github.GithubProject",
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        parameters: dict[str, Union[str, list[str]]] = {
+            "state": status.name,
+            "sort": "updated",
+            "direction": "desc",
+        }
+        if author:
+            parameters["creator"] = author
+        if assignee:
+            parameters["assignee"] = assignee
+        if labels:
+            parameters["labels"] = [
+                project.github_repo.get_label(label) for label in labels
+            ]
+
+        issues = project.github_repo.get_issues(**parameters)
+        try:
+            return [
+                GithubIssue(issue, project)
+                for issue in issues
+                if not issue.pull_request
+            ]
+        except UnknownObjectException:
+            return []
+
+    def _get_all_comments(self) -> list[IssueComment]:
+        return [
+            GithubIssueComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_issue.get_comments()
+        ]
+
+    def comment(self, body: str) -> IssueComment:
+        comment = self._raw_issue.create_comment(body)
+        return GithubIssueComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "Issue":
+        self._raw_issue.edit(state="closed")
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        for label in labels:
+            self._raw_issue.add_to_labels(label)
+
+    def add_assignee(self, *assignees: str) -> None:
+        try:
+            self._raw_issue.edit(assignees=list(assignees))
+        except github.GithubException as ex:
+            raise GithubAPIException("Failed to assign issue, unknown user") from ex
+
+    def get_comment(self, comment_id: int) -> IssueComment:
+        return GithubIssueComment(self._raw_issue.get_comment(comment_id))
+
+

Ancestors

+ +

Class variables

+
+
var raw_issue : github.Issue.Issue
+
+
+
+
+

Instance variables

+
+
var assignees : list
+
+
+
+ +Expand source code + +
@property
+def assignees(self) -> list:
+    return self._raw_issue.assignees
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/project.html b/docs/services/github/project.html new file mode 100644 index 00000000..a8c4d076 --- /dev/null +++ b/docs/services/github/project.html @@ -0,0 +1,1483 @@ + + + + + + +ogr.services.github.project API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.project

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+import logging
+from typing import ClassVar, Optional, Union
+
+import github
+from github import UnknownObjectException
+from github.Commit import Commit
+from github.CommitComment import CommitComment as GithubCommitComment
+from github.GithubException import GithubException
+from github.Repository import Repository
+
+from ogr.abstract import (
+    AccessLevel,
+    CommitComment,
+    CommitFlag,
+    CommitStatus,
+    GitTag,
+    Issue,
+    IssueStatus,
+    PRStatus,
+    PullRequest,
+    Release,
+)
+from ogr.exceptions import GithubAPIException, OperationNotSupported
+from ogr.read_only import GitProjectReadOnly, if_readonly
+from ogr.services import github as ogr_github
+from ogr.services.base import BaseGitProject
+from ogr.services.github.check_run import (
+    GithubCheckRun,
+    GithubCheckRunOutput,
+    GithubCheckRunResult,
+    GithubCheckRunStatus,
+)
+from ogr.services.github.flag import GithubCommitFlag
+from ogr.services.github.issue import GithubIssue
+from ogr.services.github.pull_request import GithubPullRequest
+from ogr.services.github.release import GithubRelease
+from ogr.utils import filter_paths, indirect
+
+logger = logging.getLogger(__name__)
+
+
+class GithubProject(BaseGitProject):
+    service: "ogr_github.GithubService"
+    # Permission levels that can merge PRs
+    CAN_MERGE_PERMS: ClassVar[set[str]] = {"admin", "write"}
+
+    def __init__(
+        self,
+        repo: str,
+        service: "ogr_github.GithubService",
+        namespace: str,
+        github_repo: Repository = None,
+        read_only: bool = False,
+        **unprocess_kwargs,
+    ) -> None:
+        if unprocess_kwargs:
+            logger.warning(
+                f"GithubProject will not process these kwargs: {unprocess_kwargs}",
+            )
+        super().__init__(repo, service, namespace)
+        self._github_repo = github_repo
+        self.read_only = read_only
+
+        self._github_instance = None
+
+    @property
+    def github_instance(self):
+        if not self._github_instance:
+            self._github_instance = self.service.get_pygithub_instance(
+                self.namespace,
+                self.repo,
+            )
+
+        return self._github_instance
+
+    @property
+    def github_repo(self):
+        if not self._github_repo:
+            self._github_repo = self.github_instance.get_repo(
+                full_name_or_id=f"{self.namespace}/{self.repo}",
+            )
+        return self._github_repo
+
+    def __str__(self) -> str:
+        return f'GithubProject(namespace="{self.namespace}", repo="{self.repo}")'
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, GithubProject):
+            return False
+
+        return (
+            self.repo == o.repo
+            and self.namespace == o.namespace
+            and self.service == o.service
+            and self.read_only == o.read_only
+        )
+
+    @property
+    def description(self) -> str:
+        return self.github_repo.description
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.github_repo.edit(description=new_description)
+
+    @property
+    def has_issues(self) -> bool:
+        return self.github_repo.has_issues
+
+    def _construct_fork_project(self) -> Optional["GithubProject"]:
+        gh_user = self.github_instance.get_user()
+        user_login = gh_user.login
+        try:
+            project = GithubProject(
+                self.repo,
+                self.service,
+                namespace=user_login,
+                read_only=self.read_only,
+            )
+            if not project.github_repo:
+                # The github_repo attribute is lazy.
+                return None
+            return project
+        except github.GithubException as ex:
+            logger.debug(f"Project {user_login}/{self.repo} does not exist: {ex}")
+            return None
+
+    def exists(self) -> bool:
+        try:
+            _ = self.github_repo
+            return True
+        except UnknownObjectException as ex:
+            if "Not Found" in str(ex):
+                return False
+            raise GithubAPIException from ex
+
+    def is_private(self) -> bool:
+        return self.github_repo.private
+
+    def is_forked(self) -> bool:
+        return bool(self._construct_fork_project())
+
+    @property
+    def is_fork(self) -> bool:
+        return self.github_repo.fork
+
+    @property
+    def parent(self) -> Optional["GithubProject"]:
+        return (
+            self.service.get_project_from_github_repository(self.github_repo.parent)
+            if self.is_fork
+            else None
+        )
+
+    @property
+    def default_branch(self):
+        return self.github_repo.default_branch
+
+    def get_branches(self) -> list[str]:
+        return [branch.name for branch in self.github_repo.get_branches()]
+
+    def get_description(self) -> str:
+        return self.github_repo.description
+
+    def add_user(self, user: str, access_level: AccessLevel) -> None:
+        access_dict = {
+            AccessLevel.pull: "Pull",
+            AccessLevel.triage: "Triage",
+            AccessLevel.push: "Push",
+            AccessLevel.admin: "Admin",
+            AccessLevel.maintain: "Maintain",
+        }
+        try:
+            invitation = self.github_repo.add_to_collaborators(
+                user,
+                permission=access_dict[access_level],
+            )
+        except Exception as ex:
+            raise GithubAPIException(f"User {user} not found") from ex
+
+        if invitation is None:
+            raise GithubAPIException("User already added")
+
+    def request_access(self):
+        raise OperationNotSupported("Not possible on GitHub")
+
+    def get_fork(self, create: bool = True) -> Optional["GithubProject"]:
+        username = self.service.user.get_username()
+        for fork in self.get_forks():
+            if fork.github_repo.owner.login == username:
+                return fork
+
+        if not self.is_forked():
+            if create:
+                return self.fork_create()
+
+            logger.info(
+                f"Fork of {self.github_repo.full_name}"
+                " does not exist and we were asked not to create it.",
+            )
+            return None
+        return self._construct_fork_project()
+
+    def get_owners(self) -> list[str]:
+        # in case of github, repository has only one owner
+        return [self.github_repo.owner.login]
+
+    def __get_collaborators(self) -> set[str]:
+        try:
+            collaborators = self._get_collaborators_with_permission()
+        except github.GithubException:
+            logger.debug(
+                "Current Github token must have push access to view repository permissions.",
+            )
+            return set()
+
+        usernames = []
+        for login, permission in collaborators.items():
+            if permission in self.CAN_MERGE_PERMS:
+                usernames.append(login)
+
+        return set(usernames)
+
+    def who_can_close_issue(self) -> set[str]:
+        return self.__get_collaborators()
+
+    def who_can_merge_pr(self) -> set[str]:
+        return self.__get_collaborators()
+
+    def can_merge_pr(self, username) -> bool:
+        return (
+            self.github_repo.get_collaborator_permission(username)
+            in self.CAN_MERGE_PERMS
+        )
+
+    def _get_collaborators_with_permission(self) -> dict:
+        """
+        Get all project collaborators in dictionary with permission association.
+
+        Returns:
+            Dictionary with logins of collaborators and their permission level.
+        """
+        collaborators = {}
+        users = self.github_repo.get_collaborators()
+        for user in users:
+            permission = self.github_repo.get_collaborator_permission(user)
+            collaborators[user.login] = permission
+        return collaborators
+
+    @indirect(GithubIssue.get_list)
+    def get_issue_list(
+        self,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list[Issue]:
+        pass
+
+    @indirect(GithubIssue.get)
+    def get_issue(self, issue_id: int) -> Issue:
+        pass
+
+    @indirect(GithubIssue.create)
+    def create_issue(
+        self,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> Issue:
+        pass
+
+    def delete(self) -> None:
+        self.github_repo.delete()
+
+    @indirect(GithubPullRequest.get_list)
+    def get_pr_list(self, status: PRStatus = PRStatus.open) -> list[PullRequest]:
+        pass
+
+    @indirect(GithubPullRequest.get)
+    def get_pr(self, pr_id: int) -> PullRequest:
+        pass
+
+    def get_sha_from_tag(self, tag_name: str) -> str:
+        # TODO: This is ugly. Can we do it better?
+        all_tags = self.github_repo.get_tags()
+        for tag in all_tags:
+            if tag.name == tag_name:
+                return tag.commit.sha
+        raise GithubAPIException(f"Tag {tag_name} was not found.")
+
+    def get_tag_from_tag_name(self, tag_name: str) -> Optional[GitTag]:
+        """
+        Get a tag based on a tag name.
+
+        Args:
+            tag_name: Name of the tag.
+
+        Returns:
+            GitTag associated with the given tag name or `None`.
+        """
+        all_tags = self.github_repo.get_tags()
+        for tag in all_tags:
+            if tag.name == tag_name:
+                return GitTag(name=tag.name, commit_sha=tag.commit.sha)
+        return None
+
+    @if_readonly(return_function=GitProjectReadOnly.create_pr)
+    @indirect(GithubPullRequest.create)
+    def create_pr(
+        self,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> PullRequest:
+        pass
+
+    @if_readonly(
+        return_function=GitProjectReadOnly.commit_comment,
+        log_message="Create Comment to commit",
+    )
+    def commit_comment(
+        self,
+        commit: str,
+        body: str,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> CommitComment:
+        github_commit: Commit = self.github_repo.get_commit(commit)
+        if filename and row:
+            comment = github_commit.create_comment(
+                body=body,
+                position=row,
+                path=filename,
+            )
+        else:
+            comment = github_commit.create_comment(body=body)
+        return self._commit_comment_from_github_object(comment)
+
+    @staticmethod
+    def _commit_comment_from_github_object(
+        raw_commit_coment: GithubCommitComment,
+    ) -> CommitComment:
+        return CommitComment(
+            body=raw_commit_coment.body,
+            author=raw_commit_coment.user.login,
+            sha=raw_commit_coment.commit_id,
+        )
+
+    def get_commit_comments(self, commit: str) -> list[CommitComment]:
+        github_commit: Commit = self.github_repo.get_commit(commit)
+        return [
+            self._commit_comment_from_github_object(comment)
+            for comment in github_commit.get_comments()
+        ]
+
+    @if_readonly(
+        return_function=GitProjectReadOnly.set_commit_status,
+        log_message="Create a status on a commit",
+    )
+    @indirect(GithubCommitFlag.set)
+    def set_commit_status(
+        self,
+        commit: str,
+        state: Union[CommitStatus, str],
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ):
+        pass
+
+    @indirect(GithubCommitFlag.get)
+    def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+        pass
+
+    @indirect(GithubCheckRun.get)
+    def get_check_run(
+        self,
+        check_run_id: Optional[int] = None,
+        commit_sha: Optional[str] = None,
+    ) -> Optional["GithubCheckRun"]:
+        pass
+
+    @indirect(GithubCheckRun.create)
+    def create_check_run(
+        self,
+        name: str,
+        commit_sha: str,
+        url: Optional[str] = None,
+        external_id: Optional[str] = None,
+        status: GithubCheckRunStatus = GithubCheckRunStatus.queued,
+        started_at: Optional[datetime.datetime] = None,
+        conclusion: Optional[GithubCheckRunResult] = None,
+        completed_at: Optional[datetime.datetime] = None,
+        output: Optional[GithubCheckRunOutput] = None,
+        actions: Optional[list[dict[str, str]]] = None,
+    ) -> "GithubCheckRun":
+        pass
+
+    @indirect(GithubCheckRun.get_list)
+    def get_check_runs(
+        self,
+        commit_sha: str,
+        name: Optional[str] = None,
+        status: Optional[GithubCheckRunStatus] = None,
+    ) -> list["GithubCheckRun"]:
+        pass
+
+    def get_git_urls(self) -> dict[str, str]:
+        return {"git": self.github_repo.clone_url, "ssh": self.github_repo.ssh_url}
+
+    @if_readonly(return_function=GitProjectReadOnly.fork_create)
+    def fork_create(self, namespace: Optional[str] = None) -> "GithubProject":
+        fork_repo = (
+            self.github_repo.create_fork(organization=namespace)
+            if namespace
+            else self.github_repo.create_fork()
+        )
+
+        fork = self.service.get_project_from_github_repository(fork_repo)
+        logger.debug(f"Forked to {fork.namespace}/{fork.repo}")
+        return fork
+
+    def change_token(self, new_token: str):
+        raise OperationNotSupported
+
+    def get_file_content(self, path: str, ref=None) -> str:
+        ref = ref or self.default_branch
+        try:
+            return self.github_repo.get_contents(
+                path=path,
+                ref=ref,
+            ).decoded_content.decode()
+        except (UnknownObjectException, GithubException) as ex:
+            if ex.status == 404:
+                raise FileNotFoundError(f"File '{path}' on {ref} not found") from ex
+            raise GithubAPIException() from ex
+
+    def get_files(
+        self,
+        ref: Optional[str] = None,
+        filter_regex: Optional[str] = None,
+        recursive: bool = False,
+    ) -> list[str]:
+        ref = ref or self.default_branch
+        paths = []
+        contents = self.github_repo.get_contents(path="", ref=ref)
+
+        if recursive:
+            while contents:
+                file_content = contents.pop(0)
+                if file_content.type == "dir":
+                    contents.extend(
+                        self.github_repo.get_contents(path=file_content.path, ref=ref),
+                    )
+                else:
+                    paths.append(file_content.path)
+
+        else:
+            paths = [
+                file_content.path
+                for file_content in contents
+                if file_content.type != "dir"
+            ]
+
+        if filter_regex:
+            paths = filter_paths(paths, filter_regex)
+
+        return paths
+
+    def get_labels(self):
+        """
+        Get list of labels in the repository.
+
+        Returns:
+            List of labels in the repository.
+        """
+        return list(self.github_repo.get_labels())
+
+    def update_labels(self, labels):
+        """
+        Update the labels of the repository. (No deletion, only add not existing ones.)
+
+        Args:
+            labels: List of labels to be added.
+
+        Returns:
+            Number of added labels.
+        """
+        current_label_names = [la.name for la in list(self.github_repo.get_labels())]
+        changes = 0
+        for label in labels:
+            if label.name not in current_label_names:
+                color = self._normalize_label_color(color=label.color)
+                self.github_repo.create_label(
+                    name=label.name,
+                    color=color,
+                    description=label.description or "",
+                )
+
+                changes += 1
+        return changes
+
+    @staticmethod
+    def _normalize_label_color(color):
+        if color.startswith("#"):
+            return color[1:]
+        return color
+
+    @indirect(GithubRelease.get)
+    def get_release(self, identifier=None, name=None, tag_name=None) -> GithubRelease:
+        pass
+
+    @indirect(GithubRelease.get_latest)
+    def get_latest_release(self) -> Optional[GithubRelease]:
+        pass
+
+    @indirect(GithubRelease.get_list)
+    def get_releases(self) -> list[Release]:
+        pass
+
+    @indirect(GithubRelease.create)
+    def create_release(self, tag: str, name: str, message: str) -> GithubRelease:
+        pass
+
+    def get_forks(self) -> list["GithubProject"]:
+        return [
+            self.service.get_project_from_github_repository(fork)
+            for fork in self.github_repo.get_forks()
+            if fork.owner
+        ]
+
+    def get_web_url(self) -> str:
+        return self.github_repo.html_url
+
+    def get_tags(self) -> list["GitTag"]:
+        return [GitTag(tag.name, tag.commit.sha) for tag in self.github_repo.get_tags()]
+
+    def get_sha_from_branch(self, branch: str) -> Optional[str]:
+        try:
+            return self.github_repo.get_branch(branch).commit.sha
+        except GithubException as ex:
+            if ex.status == 404:
+                return None
+            raise GithubAPIException from ex
+
+    def get_contributors(self) -> set[str]:
+        """
+        Returns:
+            Logins of contributors to the project.
+        """
+        return {c.login for c in self.github_repo.get_contributors()}
+
+    def users_with_write_access(self) -> set[str]:
+        return self.__get_collaborators()
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubProject +(repo: str, service: ogr_github.GithubService, namespace: str, github_repo: github.Repository.Repository = None, read_only: bool = False, **unprocess_kwargs) +
+
+

Args

+
+
repo
+
Name of the project.
+
service
+
GitService instance.
+
namespace
+
+

Namespace of the project.

+
    +
  • GitHub: username or org name.
  • +
  • GitLab: username or org name.
  • +
  • Pagure: namespace (e.g. "rpms").
  • +
+

In case of forks: "fork/{username}/{namespace}".

+
+
+
+ +Expand source code + +
class GithubProject(BaseGitProject):
+    service: "ogr_github.GithubService"
+    # Permission levels that can merge PRs
+    CAN_MERGE_PERMS: ClassVar[set[str]] = {"admin", "write"}
+
+    def __init__(
+        self,
+        repo: str,
+        service: "ogr_github.GithubService",
+        namespace: str,
+        github_repo: Repository = None,
+        read_only: bool = False,
+        **unprocess_kwargs,
+    ) -> None:
+        if unprocess_kwargs:
+            logger.warning(
+                f"GithubProject will not process these kwargs: {unprocess_kwargs}",
+            )
+        super().__init__(repo, service, namespace)
+        self._github_repo = github_repo
+        self.read_only = read_only
+
+        self._github_instance = None
+
+    @property
+    def github_instance(self):
+        if not self._github_instance:
+            self._github_instance = self.service.get_pygithub_instance(
+                self.namespace,
+                self.repo,
+            )
+
+        return self._github_instance
+
+    @property
+    def github_repo(self):
+        if not self._github_repo:
+            self._github_repo = self.github_instance.get_repo(
+                full_name_or_id=f"{self.namespace}/{self.repo}",
+            )
+        return self._github_repo
+
+    def __str__(self) -> str:
+        return f'GithubProject(namespace="{self.namespace}", repo="{self.repo}")'
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, GithubProject):
+            return False
+
+        return (
+            self.repo == o.repo
+            and self.namespace == o.namespace
+            and self.service == o.service
+            and self.read_only == o.read_only
+        )
+
+    @property
+    def description(self) -> str:
+        return self.github_repo.description
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.github_repo.edit(description=new_description)
+
+    @property
+    def has_issues(self) -> bool:
+        return self.github_repo.has_issues
+
+    def _construct_fork_project(self) -> Optional["GithubProject"]:
+        gh_user = self.github_instance.get_user()
+        user_login = gh_user.login
+        try:
+            project = GithubProject(
+                self.repo,
+                self.service,
+                namespace=user_login,
+                read_only=self.read_only,
+            )
+            if not project.github_repo:
+                # The github_repo attribute is lazy.
+                return None
+            return project
+        except github.GithubException as ex:
+            logger.debug(f"Project {user_login}/{self.repo} does not exist: {ex}")
+            return None
+
+    def exists(self) -> bool:
+        try:
+            _ = self.github_repo
+            return True
+        except UnknownObjectException as ex:
+            if "Not Found" in str(ex):
+                return False
+            raise GithubAPIException from ex
+
+    def is_private(self) -> bool:
+        return self.github_repo.private
+
+    def is_forked(self) -> bool:
+        return bool(self._construct_fork_project())
+
+    @property
+    def is_fork(self) -> bool:
+        return self.github_repo.fork
+
+    @property
+    def parent(self) -> Optional["GithubProject"]:
+        return (
+            self.service.get_project_from_github_repository(self.github_repo.parent)
+            if self.is_fork
+            else None
+        )
+
+    @property
+    def default_branch(self):
+        return self.github_repo.default_branch
+
+    def get_branches(self) -> list[str]:
+        return [branch.name for branch in self.github_repo.get_branches()]
+
+    def get_description(self) -> str:
+        return self.github_repo.description
+
+    def add_user(self, user: str, access_level: AccessLevel) -> None:
+        access_dict = {
+            AccessLevel.pull: "Pull",
+            AccessLevel.triage: "Triage",
+            AccessLevel.push: "Push",
+            AccessLevel.admin: "Admin",
+            AccessLevel.maintain: "Maintain",
+        }
+        try:
+            invitation = self.github_repo.add_to_collaborators(
+                user,
+                permission=access_dict[access_level],
+            )
+        except Exception as ex:
+            raise GithubAPIException(f"User {user} not found") from ex
+
+        if invitation is None:
+            raise GithubAPIException("User already added")
+
+    def request_access(self):
+        raise OperationNotSupported("Not possible on GitHub")
+
+    def get_fork(self, create: bool = True) -> Optional["GithubProject"]:
+        username = self.service.user.get_username()
+        for fork in self.get_forks():
+            if fork.github_repo.owner.login == username:
+                return fork
+
+        if not self.is_forked():
+            if create:
+                return self.fork_create()
+
+            logger.info(
+                f"Fork of {self.github_repo.full_name}"
+                " does not exist and we were asked not to create it.",
+            )
+            return None
+        return self._construct_fork_project()
+
+    def get_owners(self) -> list[str]:
+        # in case of github, repository has only one owner
+        return [self.github_repo.owner.login]
+
+    def __get_collaborators(self) -> set[str]:
+        try:
+            collaborators = self._get_collaborators_with_permission()
+        except github.GithubException:
+            logger.debug(
+                "Current Github token must have push access to view repository permissions.",
+            )
+            return set()
+
+        usernames = []
+        for login, permission in collaborators.items():
+            if permission in self.CAN_MERGE_PERMS:
+                usernames.append(login)
+
+        return set(usernames)
+
+    def who_can_close_issue(self) -> set[str]:
+        return self.__get_collaborators()
+
+    def who_can_merge_pr(self) -> set[str]:
+        return self.__get_collaborators()
+
+    def can_merge_pr(self, username) -> bool:
+        return (
+            self.github_repo.get_collaborator_permission(username)
+            in self.CAN_MERGE_PERMS
+        )
+
+    def _get_collaborators_with_permission(self) -> dict:
+        """
+        Get all project collaborators in dictionary with permission association.
+
+        Returns:
+            Dictionary with logins of collaborators and their permission level.
+        """
+        collaborators = {}
+        users = self.github_repo.get_collaborators()
+        for user in users:
+            permission = self.github_repo.get_collaborator_permission(user)
+            collaborators[user.login] = permission
+        return collaborators
+
+    @indirect(GithubIssue.get_list)
+    def get_issue_list(
+        self,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list[Issue]:
+        pass
+
+    @indirect(GithubIssue.get)
+    def get_issue(self, issue_id: int) -> Issue:
+        pass
+
+    @indirect(GithubIssue.create)
+    def create_issue(
+        self,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> Issue:
+        pass
+
+    def delete(self) -> None:
+        self.github_repo.delete()
+
+    @indirect(GithubPullRequest.get_list)
+    def get_pr_list(self, status: PRStatus = PRStatus.open) -> list[PullRequest]:
+        pass
+
+    @indirect(GithubPullRequest.get)
+    def get_pr(self, pr_id: int) -> PullRequest:
+        pass
+
+    def get_sha_from_tag(self, tag_name: str) -> str:
+        # TODO: This is ugly. Can we do it better?
+        all_tags = self.github_repo.get_tags()
+        for tag in all_tags:
+            if tag.name == tag_name:
+                return tag.commit.sha
+        raise GithubAPIException(f"Tag {tag_name} was not found.")
+
+    def get_tag_from_tag_name(self, tag_name: str) -> Optional[GitTag]:
+        """
+        Get a tag based on a tag name.
+
+        Args:
+            tag_name: Name of the tag.
+
+        Returns:
+            GitTag associated with the given tag name or `None`.
+        """
+        all_tags = self.github_repo.get_tags()
+        for tag in all_tags:
+            if tag.name == tag_name:
+                return GitTag(name=tag.name, commit_sha=tag.commit.sha)
+        return None
+
+    @if_readonly(return_function=GitProjectReadOnly.create_pr)
+    @indirect(GithubPullRequest.create)
+    def create_pr(
+        self,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> PullRequest:
+        pass
+
+    @if_readonly(
+        return_function=GitProjectReadOnly.commit_comment,
+        log_message="Create Comment to commit",
+    )
+    def commit_comment(
+        self,
+        commit: str,
+        body: str,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> CommitComment:
+        github_commit: Commit = self.github_repo.get_commit(commit)
+        if filename and row:
+            comment = github_commit.create_comment(
+                body=body,
+                position=row,
+                path=filename,
+            )
+        else:
+            comment = github_commit.create_comment(body=body)
+        return self._commit_comment_from_github_object(comment)
+
+    @staticmethod
+    def _commit_comment_from_github_object(
+        raw_commit_coment: GithubCommitComment,
+    ) -> CommitComment:
+        return CommitComment(
+            body=raw_commit_coment.body,
+            author=raw_commit_coment.user.login,
+            sha=raw_commit_coment.commit_id,
+        )
+
+    def get_commit_comments(self, commit: str) -> list[CommitComment]:
+        github_commit: Commit = self.github_repo.get_commit(commit)
+        return [
+            self._commit_comment_from_github_object(comment)
+            for comment in github_commit.get_comments()
+        ]
+
+    @if_readonly(
+        return_function=GitProjectReadOnly.set_commit_status,
+        log_message="Create a status on a commit",
+    )
+    @indirect(GithubCommitFlag.set)
+    def set_commit_status(
+        self,
+        commit: str,
+        state: Union[CommitStatus, str],
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ):
+        pass
+
+    @indirect(GithubCommitFlag.get)
+    def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+        pass
+
+    @indirect(GithubCheckRun.get)
+    def get_check_run(
+        self,
+        check_run_id: Optional[int] = None,
+        commit_sha: Optional[str] = None,
+    ) -> Optional["GithubCheckRun"]:
+        pass
+
+    @indirect(GithubCheckRun.create)
+    def create_check_run(
+        self,
+        name: str,
+        commit_sha: str,
+        url: Optional[str] = None,
+        external_id: Optional[str] = None,
+        status: GithubCheckRunStatus = GithubCheckRunStatus.queued,
+        started_at: Optional[datetime.datetime] = None,
+        conclusion: Optional[GithubCheckRunResult] = None,
+        completed_at: Optional[datetime.datetime] = None,
+        output: Optional[GithubCheckRunOutput] = None,
+        actions: Optional[list[dict[str, str]]] = None,
+    ) -> "GithubCheckRun":
+        pass
+
+    @indirect(GithubCheckRun.get_list)
+    def get_check_runs(
+        self,
+        commit_sha: str,
+        name: Optional[str] = None,
+        status: Optional[GithubCheckRunStatus] = None,
+    ) -> list["GithubCheckRun"]:
+        pass
+
+    def get_git_urls(self) -> dict[str, str]:
+        return {"git": self.github_repo.clone_url, "ssh": self.github_repo.ssh_url}
+
+    @if_readonly(return_function=GitProjectReadOnly.fork_create)
+    def fork_create(self, namespace: Optional[str] = None) -> "GithubProject":
+        fork_repo = (
+            self.github_repo.create_fork(organization=namespace)
+            if namespace
+            else self.github_repo.create_fork()
+        )
+
+        fork = self.service.get_project_from_github_repository(fork_repo)
+        logger.debug(f"Forked to {fork.namespace}/{fork.repo}")
+        return fork
+
+    def change_token(self, new_token: str):
+        raise OperationNotSupported
+
+    def get_file_content(self, path: str, ref=None) -> str:
+        ref = ref or self.default_branch
+        try:
+            return self.github_repo.get_contents(
+                path=path,
+                ref=ref,
+            ).decoded_content.decode()
+        except (UnknownObjectException, GithubException) as ex:
+            if ex.status == 404:
+                raise FileNotFoundError(f"File '{path}' on {ref} not found") from ex
+            raise GithubAPIException() from ex
+
+    def get_files(
+        self,
+        ref: Optional[str] = None,
+        filter_regex: Optional[str] = None,
+        recursive: bool = False,
+    ) -> list[str]:
+        ref = ref or self.default_branch
+        paths = []
+        contents = self.github_repo.get_contents(path="", ref=ref)
+
+        if recursive:
+            while contents:
+                file_content = contents.pop(0)
+                if file_content.type == "dir":
+                    contents.extend(
+                        self.github_repo.get_contents(path=file_content.path, ref=ref),
+                    )
+                else:
+                    paths.append(file_content.path)
+
+        else:
+            paths = [
+                file_content.path
+                for file_content in contents
+                if file_content.type != "dir"
+            ]
+
+        if filter_regex:
+            paths = filter_paths(paths, filter_regex)
+
+        return paths
+
+    def get_labels(self):
+        """
+        Get list of labels in the repository.
+
+        Returns:
+            List of labels in the repository.
+        """
+        return list(self.github_repo.get_labels())
+
+    def update_labels(self, labels):
+        """
+        Update the labels of the repository. (No deletion, only add not existing ones.)
+
+        Args:
+            labels: List of labels to be added.
+
+        Returns:
+            Number of added labels.
+        """
+        current_label_names = [la.name for la in list(self.github_repo.get_labels())]
+        changes = 0
+        for label in labels:
+            if label.name not in current_label_names:
+                color = self._normalize_label_color(color=label.color)
+                self.github_repo.create_label(
+                    name=label.name,
+                    color=color,
+                    description=label.description or "",
+                )
+
+                changes += 1
+        return changes
+
+    @staticmethod
+    def _normalize_label_color(color):
+        if color.startswith("#"):
+            return color[1:]
+        return color
+
+    @indirect(GithubRelease.get)
+    def get_release(self, identifier=None, name=None, tag_name=None) -> GithubRelease:
+        pass
+
+    @indirect(GithubRelease.get_latest)
+    def get_latest_release(self) -> Optional[GithubRelease]:
+        pass
+
+    @indirect(GithubRelease.get_list)
+    def get_releases(self) -> list[Release]:
+        pass
+
+    @indirect(GithubRelease.create)
+    def create_release(self, tag: str, name: str, message: str) -> GithubRelease:
+        pass
+
+    def get_forks(self) -> list["GithubProject"]:
+        return [
+            self.service.get_project_from_github_repository(fork)
+            for fork in self.github_repo.get_forks()
+            if fork.owner
+        ]
+
+    def get_web_url(self) -> str:
+        return self.github_repo.html_url
+
+    def get_tags(self) -> list["GitTag"]:
+        return [GitTag(tag.name, tag.commit.sha) for tag in self.github_repo.get_tags()]
+
+    def get_sha_from_branch(self, branch: str) -> Optional[str]:
+        try:
+            return self.github_repo.get_branch(branch).commit.sha
+        except GithubException as ex:
+            if ex.status == 404:
+                return None
+            raise GithubAPIException from ex
+
+    def get_contributors(self) -> set[str]:
+        """
+        Returns:
+            Logins of contributors to the project.
+        """
+        return {c.login for c in self.github_repo.get_contributors()}
+
+    def users_with_write_access(self) -> set[str]:
+        return self.__get_collaborators()
+
+

Ancestors

+ +

Class variables

+
+
var CAN_MERGE_PERMS : ClassVar[set[str]]
+
+
+
+
var serviceGithubService
+
+
+
+
+

Instance variables

+
+
var github_instance
+
+
+
+ +Expand source code + +
@property
+def github_instance(self):
+    if not self._github_instance:
+        self._github_instance = self.service.get_pygithub_instance(
+            self.namespace,
+            self.repo,
+        )
+
+    return self._github_instance
+
+
+
var github_repo
+
+
+
+ +Expand source code + +
@property
+def github_repo(self):
+    if not self._github_repo:
+        self._github_repo = self.github_instance.get_repo(
+            full_name_or_id=f"{self.namespace}/{self.repo}",
+        )
+    return self._github_repo
+
+
+
+

Methods

+
+
+def create_check_run(self, name: str, commit_sha: str, url: Optional[str] = None, external_id: Optional[str] = None, status: GithubCheckRunStatus = GithubCheckRunStatus.queued, started_at: Optional[datetime.datetime] = None, conclusion: Optional[GithubCheckRunResult] = None, completed_at: Optional[datetime.datetime] = None, output: Optional[dict[str, typing.Union[str, list[dict[str, typing.Union[str, int]]]]]] = None, actions: Optional[list[dict[str, str]]] = None) ‑> GithubCheckRun +
+
+
+
+ +Expand source code + +
@indirect(GithubCheckRun.create)
+def create_check_run(
+    self,
+    name: str,
+    commit_sha: str,
+    url: Optional[str] = None,
+    external_id: Optional[str] = None,
+    status: GithubCheckRunStatus = GithubCheckRunStatus.queued,
+    started_at: Optional[datetime.datetime] = None,
+    conclusion: Optional[GithubCheckRunResult] = None,
+    completed_at: Optional[datetime.datetime] = None,
+    output: Optional[GithubCheckRunOutput] = None,
+    actions: Optional[list[dict[str, str]]] = None,
+) -> "GithubCheckRun":
+    pass
+
+
+
+def get_check_run(self, check_run_id: Optional[int] = None, commit_sha: Optional[str] = None) ‑> Optional[GithubCheckRun] +
+
+
+
+ +Expand source code + +
@indirect(GithubCheckRun.get)
+def get_check_run(
+    self,
+    check_run_id: Optional[int] = None,
+    commit_sha: Optional[str] = None,
+) -> Optional["GithubCheckRun"]:
+    pass
+
+
+
+def get_check_runs(self, commit_sha: str, name: Optional[str] = None, status: Optional[GithubCheckRunStatus] = None) ‑> list['GithubCheckRun'] +
+
+
+
+ +Expand source code + +
@indirect(GithubCheckRun.get_list)
+def get_check_runs(
+    self,
+    commit_sha: str,
+    name: Optional[str] = None,
+    status: Optional[GithubCheckRunStatus] = None,
+) -> list["GithubCheckRun"]:
+    pass
+
+
+
+def get_contributors(self) ‑> set[str] +
+
+

Returns

+

Logins of contributors to the project.

+
+ +Expand source code + +
def get_contributors(self) -> set[str]:
+    """
+    Returns:
+        Logins of contributors to the project.
+    """
+    return {c.login for c in self.github_repo.get_contributors()}
+
+
+
+def get_labels(self) +
+
+

Get list of labels in the repository.

+

Returns

+

List of labels in the repository.

+
+ +Expand source code + +
def get_labels(self):
+    """
+    Get list of labels in the repository.
+
+    Returns:
+        List of labels in the repository.
+    """
+    return list(self.github_repo.get_labels())
+
+
+
+def get_tag_from_tag_name(self, tag_name: str) ‑> Optional[GitTag] +
+
+

Get a tag based on a tag name.

+

Args

+
+
tag_name
+
Name of the tag.
+
+

Returns

+

GitTag associated with the given tag name or None.

+
+ +Expand source code + +
def get_tag_from_tag_name(self, tag_name: str) -> Optional[GitTag]:
+    """
+    Get a tag based on a tag name.
+
+    Args:
+        tag_name: Name of the tag.
+
+    Returns:
+        GitTag associated with the given tag name or `None`.
+    """
+    all_tags = self.github_repo.get_tags()
+    for tag in all_tags:
+        if tag.name == tag_name:
+            return GitTag(name=tag.name, commit_sha=tag.commit.sha)
+    return None
+
+
+
+def update_labels(self, labels) +
+
+

Update the labels of the repository. (No deletion, only add not existing ones.)

+

Args

+
+
labels
+
List of labels to be added.
+
+

Returns

+

Number of added labels.

+
+ +Expand source code + +
def update_labels(self, labels):
+    """
+    Update the labels of the repository. (No deletion, only add not existing ones.)
+
+    Args:
+        labels: List of labels to be added.
+
+    Returns:
+        Number of added labels.
+    """
+    current_label_names = [la.name for la in list(self.github_repo.get_labels())]
+    changes = 0
+    for label in labels:
+        if label.name not in current_label_names:
+            color = self._normalize_label_color(color=label.color)
+            self.github_repo.create_label(
+                name=label.name,
+                color=color,
+                description=label.description or "",
+            )
+
+            changes += 1
+    return changes
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/pull_request.html b/docs/services/github/pull_request.html new file mode 100644 index 00000000..d75a5cea --- /dev/null +++ b/docs/services/github/pull_request.html @@ -0,0 +1,687 @@ + + + + + + +ogr.services.github.pull_request API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.pull_request

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+import logging
+from typing import Optional, Union
+
+import github
+import requests
+from github import UnknownObjectException
+from github.IssueComment import IssueComment as _GithubIssueComment
+from github.Label import Label as GithubLabel
+from github.PullRequest import PullRequest as _GithubPullRequest
+from github.PullRequestComment import PullRequestComment as _GithubPullRequestComment
+from github.Repository import Repository as _GithubRepository
+
+from ogr.abstract import MergeCommitStatus, PRComment, PRStatus, PullRequest
+from ogr.exceptions import GithubAPIException, OgrNetworkError
+from ogr.services import github as ogr_github
+from ogr.services.base import BasePullRequest
+from ogr.services.github.comments import GithubPRComment
+
+logger = logging.getLogger(__name__)
+
+
+class GithubPullRequest(BasePullRequest):
+    _raw_pr: _GithubPullRequest
+    _target_project: "ogr_github.GithubProject"
+    _source_project: "ogr_github.GithubProject" = None
+
+    @property
+    def title(self) -> str:
+        return self._raw_pr.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_pr.edit(title=new_title)
+
+    @property
+    def id(self) -> int:
+        return self._raw_pr.number
+
+    @property
+    def status(self) -> PRStatus:
+        return (
+            PRStatus.merged
+            if self._raw_pr.is_merged()
+            else PRStatus[self._raw_pr.state]
+        )
+
+    @property
+    def url(self) -> str:
+        return self._raw_pr.html_url
+
+    @property
+    def description(self) -> str:
+        return self._raw_pr.body
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_pr.edit(body=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_pr.user.login
+
+    @property
+    def source_branch(self) -> str:
+        return self._raw_pr.head.ref
+
+    @property
+    def target_branch(self) -> str:
+        return self._raw_pr.base.ref
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_pr.created_at
+
+    @property
+    def labels(self) -> list[GithubLabel]:
+        return list(self._raw_pr.get_labels())
+
+    @property
+    def diff_url(self) -> str:
+        return f"{self._raw_pr.html_url}/files"
+
+    @property
+    def patch(self) -> bytes:
+        response = requests.get(self._raw_pr.patch_url)
+
+        if not response.ok:
+            cls = OgrNetworkError if response.status_code >= 500 else GithubAPIException
+            raise cls(
+                f"Couldn't get patch from {self._raw_pr.patch_url} because {response.reason}.",
+            )
+
+        return response.content
+
+    @property
+    def commits_url(self) -> str:
+        return f"{self._raw_pr.html_url}/commits"
+
+    @property
+    def head_commit(self) -> str:
+        return self._raw_pr.head.sha
+
+    @property
+    def merge_commit_sha(self) -> str:
+        return self._raw_pr.merge_commit_sha
+
+    @property
+    def merge_commit_status(self) -> MergeCommitStatus:
+        if self._raw_pr.mergeable:
+            return MergeCommitStatus.can_be_merged
+
+        return MergeCommitStatus.cannot_be_merged
+
+    @property
+    def source_project(self) -> "ogr_github.GithubProject":
+        if self._source_project is None:
+            self._source_project = (
+                self._target_project.service.get_project_from_github_repository(
+                    self._raw_pr.head.repo,
+                )
+            )
+
+        return self._source_project
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        The default behavior is the pull request is made to the immediate parent repository
+        if the repository is a forked repository.
+        If you want to create a pull request to the forked repo, please pass
+        the `fork_username` parameter.
+        """
+        github_repo = project.github_repo
+
+        target_project = project
+        if project.is_fork and fork_username is None:
+            logger.warning(f"{project.full_repo_name} is fork, ignoring fork_repo.")
+            source_branch = f"{project.namespace}:{source_branch}"
+            github_repo = project.parent.github_repo
+            target_project = project.parent
+        elif fork_username:
+            source_branch = f"{fork_username}:{source_branch}"
+            if fork_username != project.namespace and project.parent is not None:
+                github_repo = GithubPullRequest.__get_fork(
+                    fork_username,
+                    project.parent.github_repo,
+                )
+
+        created_pr = github_repo.create_pull(
+            title=title,
+            body=body,
+            base=target_branch,
+            head=source_branch,
+        )
+        logger.info(f"PR {created_pr.id} created: {target_branch}<-{source_branch}")
+        return GithubPullRequest(created_pr, target_project)
+
+    @staticmethod
+    def __get_fork(fork_username: str, repo: _GithubRepository) -> _GithubRepository:
+        forks = list(
+            filter(lambda fork: fork.owner.login == fork_username, repo.get_forks()),
+        )
+        if not forks:
+            raise GithubAPIException("Requested fork doesn't exist")
+        return forks[0]
+
+    @staticmethod
+    def get(project: "ogr_github.GithubProject", pr_id: int) -> "PullRequest":
+        try:
+            pr = project.github_repo.get_pull(number=pr_id)
+        except github.UnknownObjectException as ex:
+            raise GithubAPIException(f"No pull request with id {pr_id} found") from ex
+        return GithubPullRequest(pr, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_github.GithubProject",
+        status: PRStatus = PRStatus.open,
+    ) -> list["PullRequest"]:
+        prs = project.github_repo.get_pulls(
+            # Github API has no status 'merged', just 'closed'/'opened'/'all'
+            state=status.name if status != PRStatus.merged else "closed",
+            sort="updated",
+            direction="desc",
+        )
+
+        if status == PRStatus.merged:
+            prs = list(prs)  # Github PaginatedList into list()
+            for pr in prs:
+                if not pr.is_merged():  # parse merged PRs
+                    prs.remove(pr)
+        try:
+            return [GithubPullRequest(pr, project) for pr in prs]
+        except UnknownObjectException:
+            return []
+
+    def update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "PullRequest":
+        try:
+            self._raw_pr.edit(title=title, body=description)
+            logger.info(f"PR updated: {self._raw_pr.url}")
+            return self
+        except Exception as ex:
+            raise GithubAPIException("there was an error while updating the PR") from ex
+
+    def _get_all_comments(self) -> list[PRComment]:
+        return [
+            GithubPRComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_pr.get_issue_comments()
+        ]
+
+    def get_all_commits(self) -> list[str]:
+        return [commit.sha for commit in self._raw_pr.get_commits()]
+
+    def comment(
+        self,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        comment: Union[_GithubIssueComment, _GithubPullRequestComment] = None
+        if not any([commit, filename, row]):
+            comment = self._raw_pr.create_issue_comment(body)
+        else:
+            github_commit = self._target_project.github_repo.get_commit(commit)
+            comment = self._raw_pr.create_comment(body, github_commit, filename, row)
+        return GithubPRComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "PullRequest":
+        self._raw_pr.edit(state=PRStatus.closed.name)
+        return self
+
+    def merge(self) -> "PullRequest":
+        self._raw_pr.merge()
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        for label in labels:
+            self._raw_pr.add_to_labels(label)
+
+    def get_comment(self, comment_id: int) -> PRComment:
+        return GithubPRComment(self._raw_pr.get_issue_comment(comment_id))
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubPullRequest +(raw_pr: Any, project: GitProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the pull request.
+
+
+ +Expand source code + +
class GithubPullRequest(BasePullRequest):
+    _raw_pr: _GithubPullRequest
+    _target_project: "ogr_github.GithubProject"
+    _source_project: "ogr_github.GithubProject" = None
+
+    @property
+    def title(self) -> str:
+        return self._raw_pr.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_pr.edit(title=new_title)
+
+    @property
+    def id(self) -> int:
+        return self._raw_pr.number
+
+    @property
+    def status(self) -> PRStatus:
+        return (
+            PRStatus.merged
+            if self._raw_pr.is_merged()
+            else PRStatus[self._raw_pr.state]
+        )
+
+    @property
+    def url(self) -> str:
+        return self._raw_pr.html_url
+
+    @property
+    def description(self) -> str:
+        return self._raw_pr.body
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_pr.edit(body=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_pr.user.login
+
+    @property
+    def source_branch(self) -> str:
+        return self._raw_pr.head.ref
+
+    @property
+    def target_branch(self) -> str:
+        return self._raw_pr.base.ref
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_pr.created_at
+
+    @property
+    def labels(self) -> list[GithubLabel]:
+        return list(self._raw_pr.get_labels())
+
+    @property
+    def diff_url(self) -> str:
+        return f"{self._raw_pr.html_url}/files"
+
+    @property
+    def patch(self) -> bytes:
+        response = requests.get(self._raw_pr.patch_url)
+
+        if not response.ok:
+            cls = OgrNetworkError if response.status_code >= 500 else GithubAPIException
+            raise cls(
+                f"Couldn't get patch from {self._raw_pr.patch_url} because {response.reason}.",
+            )
+
+        return response.content
+
+    @property
+    def commits_url(self) -> str:
+        return f"{self._raw_pr.html_url}/commits"
+
+    @property
+    def head_commit(self) -> str:
+        return self._raw_pr.head.sha
+
+    @property
+    def merge_commit_sha(self) -> str:
+        return self._raw_pr.merge_commit_sha
+
+    @property
+    def merge_commit_status(self) -> MergeCommitStatus:
+        if self._raw_pr.mergeable:
+            return MergeCommitStatus.can_be_merged
+
+        return MergeCommitStatus.cannot_be_merged
+
+    @property
+    def source_project(self) -> "ogr_github.GithubProject":
+        if self._source_project is None:
+            self._source_project = (
+                self._target_project.service.get_project_from_github_repository(
+                    self._raw_pr.head.repo,
+                )
+            )
+
+        return self._source_project
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        The default behavior is the pull request is made to the immediate parent repository
+        if the repository is a forked repository.
+        If you want to create a pull request to the forked repo, please pass
+        the `fork_username` parameter.
+        """
+        github_repo = project.github_repo
+
+        target_project = project
+        if project.is_fork and fork_username is None:
+            logger.warning(f"{project.full_repo_name} is fork, ignoring fork_repo.")
+            source_branch = f"{project.namespace}:{source_branch}"
+            github_repo = project.parent.github_repo
+            target_project = project.parent
+        elif fork_username:
+            source_branch = f"{fork_username}:{source_branch}"
+            if fork_username != project.namespace and project.parent is not None:
+                github_repo = GithubPullRequest.__get_fork(
+                    fork_username,
+                    project.parent.github_repo,
+                )
+
+        created_pr = github_repo.create_pull(
+            title=title,
+            body=body,
+            base=target_branch,
+            head=source_branch,
+        )
+        logger.info(f"PR {created_pr.id} created: {target_branch}<-{source_branch}")
+        return GithubPullRequest(created_pr, target_project)
+
+    @staticmethod
+    def __get_fork(fork_username: str, repo: _GithubRepository) -> _GithubRepository:
+        forks = list(
+            filter(lambda fork: fork.owner.login == fork_username, repo.get_forks()),
+        )
+        if not forks:
+            raise GithubAPIException("Requested fork doesn't exist")
+        return forks[0]
+
+    @staticmethod
+    def get(project: "ogr_github.GithubProject", pr_id: int) -> "PullRequest":
+        try:
+            pr = project.github_repo.get_pull(number=pr_id)
+        except github.UnknownObjectException as ex:
+            raise GithubAPIException(f"No pull request with id {pr_id} found") from ex
+        return GithubPullRequest(pr, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_github.GithubProject",
+        status: PRStatus = PRStatus.open,
+    ) -> list["PullRequest"]:
+        prs = project.github_repo.get_pulls(
+            # Github API has no status 'merged', just 'closed'/'opened'/'all'
+            state=status.name if status != PRStatus.merged else "closed",
+            sort="updated",
+            direction="desc",
+        )
+
+        if status == PRStatus.merged:
+            prs = list(prs)  # Github PaginatedList into list()
+            for pr in prs:
+                if not pr.is_merged():  # parse merged PRs
+                    prs.remove(pr)
+        try:
+            return [GithubPullRequest(pr, project) for pr in prs]
+        except UnknownObjectException:
+            return []
+
+    def update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "PullRequest":
+        try:
+            self._raw_pr.edit(title=title, body=description)
+            logger.info(f"PR updated: {self._raw_pr.url}")
+            return self
+        except Exception as ex:
+            raise GithubAPIException("there was an error while updating the PR") from ex
+
+    def _get_all_comments(self) -> list[PRComment]:
+        return [
+            GithubPRComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_pr.get_issue_comments()
+        ]
+
+    def get_all_commits(self) -> list[str]:
+        return [commit.sha for commit in self._raw_pr.get_commits()]
+
+    def comment(
+        self,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        comment: Union[_GithubIssueComment, _GithubPullRequestComment] = None
+        if not any([commit, filename, row]):
+            comment = self._raw_pr.create_issue_comment(body)
+        else:
+            github_commit = self._target_project.github_repo.get_commit(commit)
+            comment = self._raw_pr.create_comment(body, github_commit, filename, row)
+        return GithubPRComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "PullRequest":
+        self._raw_pr.edit(state=PRStatus.closed.name)
+        return self
+
+    def merge(self) -> "PullRequest":
+        self._raw_pr.merge()
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        for label in labels:
+            self._raw_pr.add_to_labels(label)
+
+    def get_comment(self, comment_id: int) -> PRComment:
+        return GithubPRComment(self._raw_pr.get_issue_comment(comment_id))
+
+

Ancestors

+ +

Static methods

+
+
+def create(project: ogr_github.GithubProject, title: str, body: str, target_branch: str, source_branch: str, fork_username: Optional[str] = None) ‑> PullRequest +
+
+

The default behavior is the pull request is made to the immediate parent repository +if the repository is a forked repository. +If you want to create a pull request to the forked repo, please pass +the fork_username parameter.

+
+ +Expand source code + +
@staticmethod
+def create(
+    project: "ogr_github.GithubProject",
+    title: str,
+    body: str,
+    target_branch: str,
+    source_branch: str,
+    fork_username: Optional[str] = None,
+) -> "PullRequest":
+    """
+    The default behavior is the pull request is made to the immediate parent repository
+    if the repository is a forked repository.
+    If you want to create a pull request to the forked repo, please pass
+    the `fork_username` parameter.
+    """
+    github_repo = project.github_repo
+
+    target_project = project
+    if project.is_fork and fork_username is None:
+        logger.warning(f"{project.full_repo_name} is fork, ignoring fork_repo.")
+        source_branch = f"{project.namespace}:{source_branch}"
+        github_repo = project.parent.github_repo
+        target_project = project.parent
+    elif fork_username:
+        source_branch = f"{fork_username}:{source_branch}"
+        if fork_username != project.namespace and project.parent is not None:
+            github_repo = GithubPullRequest.__get_fork(
+                fork_username,
+                project.parent.github_repo,
+            )
+
+    created_pr = github_repo.create_pull(
+        title=title,
+        body=body,
+        base=target_branch,
+        head=source_branch,
+    )
+    logger.info(f"PR {created_pr.id} created: {target_branch}<-{source_branch}")
+    return GithubPullRequest(created_pr, target_project)
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/release.html b/docs/services/github/release.html new file mode 100644 index 00000000..032d54b9 --- /dev/null +++ b/docs/services/github/release.html @@ -0,0 +1,388 @@ + + + + + + +ogr.services.github.release API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.release

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from typing import Optional
+
+from github import GithubException
+from github.GitRelease import GitRelease as PyGithubRelease
+
+from ogr.abstract import GitTag, Release
+from ogr.exceptions import GithubAPIException
+from ogr.services import github as ogr_github
+
+
+class GithubRelease(Release):
+    _raw_release: PyGithubRelease
+    project: "ogr_github.GithubProject"
+
+    @staticmethod
+    def _release_id_from_name(
+        project: "ogr_github.GithubProject",
+        name: str,
+    ) -> Optional[int]:
+        releases = project.github_repo.get_releases()
+        for release in releases:
+            if release.title == name:
+                return release.id
+        return None
+
+    @staticmethod
+    def _release_id_from_tag(
+        project: "ogr_github.GithubProject",
+        tag: str,
+    ) -> Optional[int]:
+        releases = project.github_repo.get_releases()
+        for release in releases:
+            if release.tag_name == tag:
+                return release.id
+        return None
+
+    @property
+    def title(self):
+        return self._raw_release.title
+
+    @property
+    def body(self):
+        return self._raw_release.body
+
+    @property
+    def git_tag(self) -> GitTag:
+        return self.project.get_tag_from_tag_name(self.tag_name)
+
+    @property
+    def tag_name(self) -> str:
+        return self._raw_release.tag_name
+
+    @property
+    def url(self) -> Optional[str]:
+        return self._raw_release.html_url
+
+    @property
+    def created_at(self) -> datetime.datetime:
+        return self._raw_release.created_at
+
+    @property
+    def tarball_url(self) -> str:
+        return self._raw_release.tarball_url
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    @staticmethod
+    def get(
+        project: "ogr_github.GithubProject",
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> "Release":
+        if tag_name:
+            identifier = GithubRelease._release_id_from_tag(project, tag_name)
+        elif name:
+            identifier = GithubRelease._release_id_from_name(project, name)
+        if identifier is None:
+            raise GithubAPIException("Release was not found.")
+        release = project.github_repo.get_release(id=identifier)
+        return GithubRelease(release, project)
+
+    @staticmethod
+    def get_latest(project: "ogr_github.GithubProject") -> Optional["Release"]:
+        try:
+            release = project.github_repo.get_latest_release()
+            return GithubRelease(release, project)
+        except GithubException as ex:
+            if ex.status == 404:
+                return None
+            raise GithubAPIException from ex
+
+    @staticmethod
+    def get_list(project: "ogr_github.GithubProject") -> list["Release"]:
+        releases = project.github_repo.get_releases()
+        return [GithubRelease(release, project) for release in releases]
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> "Release":
+        created_release = project.github_repo.create_git_release(
+            tag=tag,
+            name=name,
+            message=message,
+        )
+        return GithubRelease(created_release, project)
+
+    def edit_release(self, name: str, message: str) -> None:
+        """
+        Edit name and message of a release.
+
+        Args:
+            name: New name of the release.
+            message: New message for the release.
+        """
+        self._raw_release = self._raw_release.update_release(name=name, message=message)
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubRelease +(raw_release: Any, project: GitProject) +
+
+

Object that represents release.

+

Attributes

+
+
project : GitProject
+
Project on which the release is created.
+
+
+ +Expand source code + +
class GithubRelease(Release):
+    _raw_release: PyGithubRelease
+    project: "ogr_github.GithubProject"
+
+    @staticmethod
+    def _release_id_from_name(
+        project: "ogr_github.GithubProject",
+        name: str,
+    ) -> Optional[int]:
+        releases = project.github_repo.get_releases()
+        for release in releases:
+            if release.title == name:
+                return release.id
+        return None
+
+    @staticmethod
+    def _release_id_from_tag(
+        project: "ogr_github.GithubProject",
+        tag: str,
+    ) -> Optional[int]:
+        releases = project.github_repo.get_releases()
+        for release in releases:
+            if release.tag_name == tag:
+                return release.id
+        return None
+
+    @property
+    def title(self):
+        return self._raw_release.title
+
+    @property
+    def body(self):
+        return self._raw_release.body
+
+    @property
+    def git_tag(self) -> GitTag:
+        return self.project.get_tag_from_tag_name(self.tag_name)
+
+    @property
+    def tag_name(self) -> str:
+        return self._raw_release.tag_name
+
+    @property
+    def url(self) -> Optional[str]:
+        return self._raw_release.html_url
+
+    @property
+    def created_at(self) -> datetime.datetime:
+        return self._raw_release.created_at
+
+    @property
+    def tarball_url(self) -> str:
+        return self._raw_release.tarball_url
+
+    def __str__(self) -> str:
+        return "Github" + super().__str__()
+
+    @staticmethod
+    def get(
+        project: "ogr_github.GithubProject",
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> "Release":
+        if tag_name:
+            identifier = GithubRelease._release_id_from_tag(project, tag_name)
+        elif name:
+            identifier = GithubRelease._release_id_from_name(project, name)
+        if identifier is None:
+            raise GithubAPIException("Release was not found.")
+        release = project.github_repo.get_release(id=identifier)
+        return GithubRelease(release, project)
+
+    @staticmethod
+    def get_latest(project: "ogr_github.GithubProject") -> Optional["Release"]:
+        try:
+            release = project.github_repo.get_latest_release()
+            return GithubRelease(release, project)
+        except GithubException as ex:
+            if ex.status == 404:
+                return None
+            raise GithubAPIException from ex
+
+    @staticmethod
+    def get_list(project: "ogr_github.GithubProject") -> list["Release"]:
+        releases = project.github_repo.get_releases()
+        return [GithubRelease(release, project) for release in releases]
+
+    @staticmethod
+    def create(
+        project: "ogr_github.GithubProject",
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> "Release":
+        created_release = project.github_repo.create_git_release(
+            tag=tag,
+            name=name,
+            message=message,
+        )
+        return GithubRelease(created_release, project)
+
+    def edit_release(self, name: str, message: str) -> None:
+        """
+        Edit name and message of a release.
+
+        Args:
+            name: New name of the release.
+            message: New message for the release.
+        """
+        self._raw_release = self._raw_release.update_release(name=name, message=message)
+
+

Ancestors

+ +

Class variables

+
+
var projectGithubProject
+
+
+
+
+

Methods

+
+
+def edit_release(self, name: str, message: str) ‑> None +
+
+

Edit name and message of a release.

+

Args

+
+
name
+
New name of the release.
+
message
+
New message for the release.
+
+
+ +Expand source code + +
def edit_release(self, name: str, message: str) -> None:
+    """
+    Edit name and message of a release.
+
+    Args:
+        name: New name of the release.
+        message: New message for the release.
+    """
+    self._raw_release = self._raw_release.update_release(name=name, message=message)
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/service.html b/docs/services/github/service.html new file mode 100644 index 00000000..c222ece0 --- /dev/null +++ b/docs/services/github/service.html @@ -0,0 +1,665 @@ + + + + + + +ogr.services.github.service API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.service

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import logging
+import re
+from typing import Optional, Union
+
+import github
+import github.GithubObject
+from github import (
+    Github as PyGithubInstance,
+)
+from github import (
+    Repository as PyGithubRepository,
+)
+from github import (
+    UnknownObjectException,
+)
+from urllib3.util import Retry
+
+from ogr.abstract import AuthMethod, GitUser
+from ogr.exceptions import GithubAPIException
+from ogr.factory import use_for_service
+from ogr.services.base import BaseGitService, GitProject
+from ogr.services.github.auth_providers import (
+    GithubApp,
+    GithubAuthentication,
+    TokenAuthentication,
+    Tokman,
+)
+from ogr.services.github.project import GithubProject
+from ogr.services.github.user import GithubUser
+
+logger = logging.getLogger(__name__)
+
+
+@use_for_service("github.com")
+class GithubService(BaseGitService):
+    # class parameter could be used to mock Github class api
+    github_class: type[github.Github]
+    instance_url = "https://github.com"
+
+    def __init__(
+        self,
+        token=None,
+        read_only=False,
+        github_app_id: Optional[str] = None,
+        github_app_private_key: Optional[str] = None,
+        github_app_private_key_path: Optional[str] = None,
+        tokman_instance_url: Optional[str] = None,
+        github_authentication: GithubAuthentication = None,
+        max_retries: Union[int, Retry] = 0,
+        **kwargs,
+    ):
+        """
+        If multiple authentication methods are provided, they are prioritised:
+            1. Tokman
+            2. GithubApp
+            3. TokenAuthentication (which is also default one, that works without specified token)
+        """
+        super().__init__()
+        self.read_only = read_only
+        self._default_auth_method = github_authentication
+        self._other_auth_method: GithubAuthentication = None
+        self._auth_methods: dict[AuthMethod, GithubAuthentication] = {}
+
+        if isinstance(max_retries, Retry):
+            self._max_retries = max_retries
+        else:
+            self._max_retries = Retry(
+                total=int(max_retries),
+                read=0,
+                # Retry mechanism active for these HTTP methods:
+                allowed_methods=["DELETE", "GET", "PATCH", "POST", "PUT"],
+                # Only retry on following HTTP status codes
+                status_forcelist=[500, 503, 403, 401],
+                raise_on_status=False,
+            )
+
+        if not self._default_auth_method:
+            self.__set_authentication(
+                token=token,
+                github_app_id=github_app_id,
+                github_app_private_key=github_app_private_key,
+                github_app_private_key_path=github_app_private_key_path,
+                tokman_instance_url=tokman_instance_url,
+                max_retries=self._max_retries,
+            )
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    def __set_authentication(self, **kwargs):
+        auth_methods = [
+            (Tokman, AuthMethod.tokman),
+            (GithubApp, AuthMethod.github_app),
+            (TokenAuthentication, AuthMethod.token),
+        ]
+        for auth_class, auth_name in auth_methods:
+            auth_inst = auth_class.try_create(**kwargs)
+            self._auth_methods[auth_name] = auth_inst
+            if not self._default_auth_method:
+                self._default_auth_method = auth_inst
+
+        return None if self._default_auth_method else TokenAuthentication(None)
+
+    def set_auth_method(self, method: AuthMethod):
+        if self._auth_methods[method]:
+            logger.info("Forced Github auth method to %s", method)
+            self._other_auth_method = self._auth_methods[method]
+        else:
+            raise GithubAPIException(
+                f"Choosen authentication method ({method}) is not available",
+            )
+
+    def reset_auth_method(self):
+        logger.info("Reset Github auth method to the default")
+        self._other_auth_method = None
+
+    @property
+    def authentication(self):
+        return self._other_auth_method or self._default_auth_method
+
+    @property
+    def github(self):
+        return self.authentication.pygithub_instance
+
+    def __str__(self) -> str:
+        readonly_str = ", read_only=True" if self.read_only else ""
+        arguments = f", github_authentication={self.authentication!s}{readonly_str}"
+
+        if arguments:
+            # remove the first '- '
+            arguments = arguments[2:]
+
+        return f"GithubService({arguments})"
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, GithubService):
+            return False
+
+        return (
+            self.read_only == o.read_only  # type: ignore
+            and self.authentication == o.authentication  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(
+        self,
+        repo=None,
+        namespace=None,
+        is_fork=False,
+        **kwargs,
+    ) -> "GithubProject":
+        if is_fork:
+            namespace = self.user.get_username()
+        return GithubProject(
+            repo=repo,
+            namespace=namespace,
+            service=self,
+            read_only=self.read_only,
+            **kwargs,
+        )
+
+    def get_project_from_github_repository(
+        self,
+        github_repo: PyGithubRepository.Repository,
+    ) -> "GithubProject":
+        return GithubProject(
+            repo=github_repo.name,
+            namespace=github_repo.owner.login,
+            github_repo=github_repo,
+            service=self,
+            read_only=self.read_only,
+        )
+
+    @property
+    def user(self) -> GitUser:
+        return GithubUser(service=self)
+
+    def change_token(self, new_token: str) -> None:
+        self._default_auth_method = TokenAuthentication(new_token)
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "GithubProject":
+        if namespace:
+            try:
+                owner = self.github.get_organization(namespace)
+            except UnknownObjectException as ex:
+                raise GithubAPIException(f"Group {namespace} not found.") from ex
+        else:
+            owner = self.github.get_user()
+
+        try:
+            new_repo = owner.create_repo(
+                name=repo,
+                description=description if description else github.GithubObject.NotSet,
+            )
+        except github.GithubException as ex:
+            raise GithubAPIException("Project creation failed") from ex
+        return GithubProject(
+            repo=repo,
+            namespace=namespace or owner.login,
+            service=self,
+            github_repo=new_repo,
+        )
+
+    def get_pygithub_instance(self, namespace: str, repo: str) -> PyGithubInstance:
+        token = self.authentication.get_token(namespace, repo)
+        return PyGithubInstance(login_or_token=token, retry=self._max_retries)
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        search_query = ""
+
+        if user:
+            search_query += f"user:{user}"
+
+        if language:
+            search_query += f" language:{language}"
+
+        projects: list[GitProject]
+        projects = [
+            GithubProject(
+                repo=repo.name,
+                namespace=repo.owner.login,
+                github_repo=repo,
+                service=self,
+            )
+            for repo in self.github.search_repositories(search_query, order="asc")
+        ]
+
+        if search_pattern:
+            projects = [
+                project
+                for project in projects
+                if re.search(search_pattern, project.repo)
+            ]
+
+        return projects
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubService +(token=None, read_only=False, github_app_id: Optional[str] = None, github_app_private_key: Optional[str] = None, github_app_private_key_path: Optional[str] = None, tokman_instance_url: Optional[str] = None, github_authentication: GithubAuthentication = None, max_retries: Union[int, urllib3.util.retry.Retry] = 0, **kwargs) +
+
+

Attributes

+
+
instance_url : str
+
URL of the git forge instance.
+
+

If multiple authentication methods are provided, they are prioritised: +1. Tokman +2. GithubApp +3. TokenAuthentication (which is also default one, that works without specified token)

+
+ +Expand source code + +
@use_for_service("github.com")
+class GithubService(BaseGitService):
+    # class parameter could be used to mock Github class api
+    github_class: type[github.Github]
+    instance_url = "https://github.com"
+
+    def __init__(
+        self,
+        token=None,
+        read_only=False,
+        github_app_id: Optional[str] = None,
+        github_app_private_key: Optional[str] = None,
+        github_app_private_key_path: Optional[str] = None,
+        tokman_instance_url: Optional[str] = None,
+        github_authentication: GithubAuthentication = None,
+        max_retries: Union[int, Retry] = 0,
+        **kwargs,
+    ):
+        """
+        If multiple authentication methods are provided, they are prioritised:
+            1. Tokman
+            2. GithubApp
+            3. TokenAuthentication (which is also default one, that works without specified token)
+        """
+        super().__init__()
+        self.read_only = read_only
+        self._default_auth_method = github_authentication
+        self._other_auth_method: GithubAuthentication = None
+        self._auth_methods: dict[AuthMethod, GithubAuthentication] = {}
+
+        if isinstance(max_retries, Retry):
+            self._max_retries = max_retries
+        else:
+            self._max_retries = Retry(
+                total=int(max_retries),
+                read=0,
+                # Retry mechanism active for these HTTP methods:
+                allowed_methods=["DELETE", "GET", "PATCH", "POST", "PUT"],
+                # Only retry on following HTTP status codes
+                status_forcelist=[500, 503, 403, 401],
+                raise_on_status=False,
+            )
+
+        if not self._default_auth_method:
+            self.__set_authentication(
+                token=token,
+                github_app_id=github_app_id,
+                github_app_private_key=github_app_private_key,
+                github_app_private_key_path=github_app_private_key_path,
+                tokman_instance_url=tokman_instance_url,
+                max_retries=self._max_retries,
+            )
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    def __set_authentication(self, **kwargs):
+        auth_methods = [
+            (Tokman, AuthMethod.tokman),
+            (GithubApp, AuthMethod.github_app),
+            (TokenAuthentication, AuthMethod.token),
+        ]
+        for auth_class, auth_name in auth_methods:
+            auth_inst = auth_class.try_create(**kwargs)
+            self._auth_methods[auth_name] = auth_inst
+            if not self._default_auth_method:
+                self._default_auth_method = auth_inst
+
+        return None if self._default_auth_method else TokenAuthentication(None)
+
+    def set_auth_method(self, method: AuthMethod):
+        if self._auth_methods[method]:
+            logger.info("Forced Github auth method to %s", method)
+            self._other_auth_method = self._auth_methods[method]
+        else:
+            raise GithubAPIException(
+                f"Choosen authentication method ({method}) is not available",
+            )
+
+    def reset_auth_method(self):
+        logger.info("Reset Github auth method to the default")
+        self._other_auth_method = None
+
+    @property
+    def authentication(self):
+        return self._other_auth_method or self._default_auth_method
+
+    @property
+    def github(self):
+        return self.authentication.pygithub_instance
+
+    def __str__(self) -> str:
+        readonly_str = ", read_only=True" if self.read_only else ""
+        arguments = f", github_authentication={self.authentication!s}{readonly_str}"
+
+        if arguments:
+            # remove the first '- '
+            arguments = arguments[2:]
+
+        return f"GithubService({arguments})"
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, GithubService):
+            return False
+
+        return (
+            self.read_only == o.read_only  # type: ignore
+            and self.authentication == o.authentication  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(
+        self,
+        repo=None,
+        namespace=None,
+        is_fork=False,
+        **kwargs,
+    ) -> "GithubProject":
+        if is_fork:
+            namespace = self.user.get_username()
+        return GithubProject(
+            repo=repo,
+            namespace=namespace,
+            service=self,
+            read_only=self.read_only,
+            **kwargs,
+        )
+
+    def get_project_from_github_repository(
+        self,
+        github_repo: PyGithubRepository.Repository,
+    ) -> "GithubProject":
+        return GithubProject(
+            repo=github_repo.name,
+            namespace=github_repo.owner.login,
+            github_repo=github_repo,
+            service=self,
+            read_only=self.read_only,
+        )
+
+    @property
+    def user(self) -> GitUser:
+        return GithubUser(service=self)
+
+    def change_token(self, new_token: str) -> None:
+        self._default_auth_method = TokenAuthentication(new_token)
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "GithubProject":
+        if namespace:
+            try:
+                owner = self.github.get_organization(namespace)
+            except UnknownObjectException as ex:
+                raise GithubAPIException(f"Group {namespace} not found.") from ex
+        else:
+            owner = self.github.get_user()
+
+        try:
+            new_repo = owner.create_repo(
+                name=repo,
+                description=description if description else github.GithubObject.NotSet,
+            )
+        except github.GithubException as ex:
+            raise GithubAPIException("Project creation failed") from ex
+        return GithubProject(
+            repo=repo,
+            namespace=namespace or owner.login,
+            service=self,
+            github_repo=new_repo,
+        )
+
+    def get_pygithub_instance(self, namespace: str, repo: str) -> PyGithubInstance:
+        token = self.authentication.get_token(namespace, repo)
+        return PyGithubInstance(login_or_token=token, retry=self._max_retries)
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        search_query = ""
+
+        if user:
+            search_query += f"user:{user}"
+
+        if language:
+            search_query += f" language:{language}"
+
+        projects: list[GitProject]
+        projects = [
+            GithubProject(
+                repo=repo.name,
+                namespace=repo.owner.login,
+                github_repo=repo,
+                service=self,
+            )
+            for repo in self.github.search_repositories(search_query, order="asc")
+        ]
+
+        if search_pattern:
+            projects = [
+                project
+                for project in projects
+                if re.search(search_pattern, project.repo)
+            ]
+
+        return projects
+
+

Ancestors

+ +

Class variables

+
+
var github_class : type[github.MainClass.Github]
+
+
+
+
var instance_url : Optional[str]
+
+
+
+
+

Instance variables

+
+
var authentication
+
+
+
+ +Expand source code + +
@property
+def authentication(self):
+    return self._other_auth_method or self._default_auth_method
+
+
+
var github
+
+
+
+ +Expand source code + +
@property
+def github(self):
+    return self.authentication.pygithub_instance
+
+
+
+

Methods

+
+
+def get_project_from_github_repository(self, github_repo: github.Repository.Repository) ‑> GithubProject +
+
+
+
+ +Expand source code + +
def get_project_from_github_repository(
+    self,
+    github_repo: PyGithubRepository.Repository,
+) -> "GithubProject":
+    return GithubProject(
+        repo=github_repo.name,
+        namespace=github_repo.owner.login,
+        github_repo=github_repo,
+        service=self,
+        read_only=self.read_only,
+    )
+
+
+
+def get_pygithub_instance(self, namespace: str, repo: str) ‑> github.MainClass.Github +
+
+
+
+ +Expand source code + +
def get_pygithub_instance(self, namespace: str, repo: str) -> PyGithubInstance:
+    token = self.authentication.get_token(namespace, repo)
+    return PyGithubInstance(login_or_token=token, retry=self._max_retries)
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/github/user.html b/docs/services/github/user.html new file mode 100644 index 00000000..cc02f9b2 --- /dev/null +++ b/docs/services/github/user.html @@ -0,0 +1,224 @@ + + + + + + +ogr.services.github.user API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.github.user

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from collections import namedtuple
+from typing import Optional
+
+from ogr.services import github as ogr_github
+from ogr.services.base import BaseGitUser
+from ogr.services.github.project import GithubProject
+
+
+class GithubUser(BaseGitUser):
+    service: "ogr_github.GithubService"
+
+    def __init__(self, service: "ogr_github.GithubService") -> None:
+        super().__init__(service=service)
+
+    def __str__(self) -> str:
+        return f'GithubUser(username="{self.get_username()}")'
+
+    @property
+    def _github_user(self):
+        return self.service.github.get_user()
+
+    def get_username(self) -> str:
+        return self.service.github.get_user().login
+
+    def get_email(self) -> Optional[str]:
+        user_email_property = self.service.github.get_user().email
+        if user_email_property:
+            return user_email_property
+
+        user_emails = self.service.github.get_user().get_emails()
+
+        if not user_emails:
+            return None
+
+        # To work around the braking change introduced by pygithub==1.55
+        # https://pygithub.readthedocs.io/en/latest/changes.html#version-1-55-april-26-2021
+        if isinstance(user_emails[0], dict):
+            EmailData = namedtuple("EmailData", user_emails[0].keys())  # type: ignore
+        for email in user_emails:
+            if "EmailData" in locals():
+                email = EmailData(**email)  # type: ignore
+            if email.primary:
+                return email.email
+
+        # Return the first email we received
+        return user_emails[0]["email"]
+
+    def get_projects(self) -> list["ogr_github.GithubProject"]:
+        raw_repos = self._github_user.get_repos(affiliation="owner")
+        return [
+            GithubProject(
+                repo=repo.name,
+                namespace=repo.owner.login,
+                github_repo=repo,
+                service=self.service,
+            )
+            for repo in raw_repos
+        ]
+
+    def get_forks(self) -> list["ogr_github.GithubProject"]:
+        return [project for project in self.get_projects() if project.github_repo.fork]
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GithubUser +(service: ogr_github.GithubService) +
+
+

Represents currently authenticated user through service.

+
+ +Expand source code + +
class GithubUser(BaseGitUser):
+    service: "ogr_github.GithubService"
+
+    def __init__(self, service: "ogr_github.GithubService") -> None:
+        super().__init__(service=service)
+
+    def __str__(self) -> str:
+        return f'GithubUser(username="{self.get_username()}")'
+
+    @property
+    def _github_user(self):
+        return self.service.github.get_user()
+
+    def get_username(self) -> str:
+        return self.service.github.get_user().login
+
+    def get_email(self) -> Optional[str]:
+        user_email_property = self.service.github.get_user().email
+        if user_email_property:
+            return user_email_property
+
+        user_emails = self.service.github.get_user().get_emails()
+
+        if not user_emails:
+            return None
+
+        # To work around the braking change introduced by pygithub==1.55
+        # https://pygithub.readthedocs.io/en/latest/changes.html#version-1-55-april-26-2021
+        if isinstance(user_emails[0], dict):
+            EmailData = namedtuple("EmailData", user_emails[0].keys())  # type: ignore
+        for email in user_emails:
+            if "EmailData" in locals():
+                email = EmailData(**email)  # type: ignore
+            if email.primary:
+                return email.email
+
+        # Return the first email we received
+        return user_emails[0]["email"]
+
+    def get_projects(self) -> list["ogr_github.GithubProject"]:
+        raw_repos = self._github_user.get_repos(affiliation="owner")
+        return [
+            GithubProject(
+                repo=repo.name,
+                namespace=repo.owner.login,
+                github_repo=repo,
+                service=self.service,
+            )
+            for repo in raw_repos
+        ]
+
+    def get_forks(self) -> list["ogr_github.GithubProject"]:
+        return [project for project in self.get_projects() if project.github_repo.fork]
+
+

Ancestors

+ +

Class variables

+
+
var serviceGithubService
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/gitlab/comments.html b/docs/services/gitlab/comments.html new file mode 100644 index 00000000..49ef77d3 --- /dev/null +++ b/docs/services/gitlab/comments.html @@ -0,0 +1,364 @@ + + + + + + +ogr.services.gitlab.comments API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.gitlab.comments

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+import logging
+from typing import Union
+
+import gitlab.exceptions
+from gitlab.v4.objects import (
+    ProjectIssueNote,
+    ProjectIssueNoteAwardEmoji,
+    ProjectMergeRequestAwardEmoji,
+    ProjectMergeRequestNote,
+)
+
+from ogr.abstract import Comment, IssueComment, PRComment, Reaction
+from ogr.exceptions import GitlabAPIException
+
+logger = logging.getLogger(__name__)
+
+
+class GitlabReaction(Reaction):
+    _raw_reaction: Union[ProjectIssueNoteAwardEmoji, ProjectMergeRequestAwardEmoji]
+
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+    def delete(self) -> None:
+        self._raw_reaction.delete()
+
+
+class GitlabComment(Comment):
+    def _from_raw_comment(
+        self,
+        raw_comment: Union[ProjectIssueNote, ProjectMergeRequestNote],
+    ) -> None:
+        self._raw_comment = raw_comment
+        self._id = raw_comment.get_id()
+        self._author = raw_comment.author["username"]
+        self._created = raw_comment.created_at
+
+    @property
+    def body(self) -> str:
+        return self._raw_comment.body
+
+    @body.setter
+    def body(self, new_body: str) -> None:
+        self._raw_comment.body = new_body
+        self._raw_comment.save()
+
+    @property
+    def edited(self) -> datetime.datetime:
+        return self._raw_comment.updated_at
+
+    def get_reactions(self) -> list[Reaction]:
+        return [
+            GitlabReaction(reaction)
+            for reaction in self._raw_comment.awardemojis.list()
+        ]
+
+    def add_reaction(self, reaction: str) -> GitlabReaction:
+        try:
+            reaction_obj = self._raw_comment.awardemojis.create({"name": reaction})
+        except gitlab.exceptions.GitlabCreateError as ex:
+            if "404 Award Emoji Name has already been taken" not in str(ex):
+                raise GitlabAPIException() from ex
+
+            # this happens only when the reaction was already added
+            logger.info(f"The emoji {reaction} has already been taken.")
+            (reaction_obj,) = filter(
+                (
+                    # we want to return that already given reaction
+                    lambda item: item.attributes["name"] == reaction
+                    and item.attributes["user"]["name"]
+                    == item.awardemojis.gitlab.user.name
+                ),
+                self._raw_comment.awardemojis.list(),
+            )
+
+        return GitlabReaction(reaction_obj)
+
+
+class GitlabIssueComment(GitlabComment, IssueComment):
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+
+class GitlabPRComment(GitlabComment, PRComment):
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GitlabComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class GitlabComment(Comment):
+    def _from_raw_comment(
+        self,
+        raw_comment: Union[ProjectIssueNote, ProjectMergeRequestNote],
+    ) -> None:
+        self._raw_comment = raw_comment
+        self._id = raw_comment.get_id()
+        self._author = raw_comment.author["username"]
+        self._created = raw_comment.created_at
+
+    @property
+    def body(self) -> str:
+        return self._raw_comment.body
+
+    @body.setter
+    def body(self, new_body: str) -> None:
+        self._raw_comment.body = new_body
+        self._raw_comment.save()
+
+    @property
+    def edited(self) -> datetime.datetime:
+        return self._raw_comment.updated_at
+
+    def get_reactions(self) -> list[Reaction]:
+        return [
+            GitlabReaction(reaction)
+            for reaction in self._raw_comment.awardemojis.list()
+        ]
+
+    def add_reaction(self, reaction: str) -> GitlabReaction:
+        try:
+            reaction_obj = self._raw_comment.awardemojis.create({"name": reaction})
+        except gitlab.exceptions.GitlabCreateError as ex:
+            if "404 Award Emoji Name has already been taken" not in str(ex):
+                raise GitlabAPIException() from ex
+
+            # this happens only when the reaction was already added
+            logger.info(f"The emoji {reaction} has already been taken.")
+            (reaction_obj,) = filter(
+                (
+                    # we want to return that already given reaction
+                    lambda item: item.attributes["name"] == reaction
+                    and item.attributes["user"]["name"]
+                    == item.awardemojis.gitlab.user.name
+                ),
+                self._raw_comment.awardemojis.list(),
+            )
+
+        return GitlabReaction(reaction_obj)
+
+

Ancestors

+ +

Subclasses

+ +

Inherited members

+ +
+
+class GitlabIssueComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class GitlabIssueComment(GitlabComment, IssueComment):
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+class GitlabPRComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class GitlabPRComment(GitlabComment, PRComment):
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+class GitlabReaction +(raw_reaction: Any) +
+
+
+
+ +Expand source code + +
class GitlabReaction(Reaction):
+    _raw_reaction: Union[ProjectIssueNoteAwardEmoji, ProjectMergeRequestAwardEmoji]
+
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+    def delete(self) -> None:
+        self._raw_reaction.delete()
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/gitlab/flag.html b/docs/services/gitlab/flag.html new file mode 100644 index 00000000..443d0aab --- /dev/null +++ b/docs/services/gitlab/flag.html @@ -0,0 +1,294 @@ + + + + + + +ogr.services.gitlab.flag API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.gitlab.flag

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+import logging
+from typing import ClassVar
+
+import gitlab
+
+from ogr.abstract import CommitFlag, CommitStatus
+from ogr.exceptions import GitlabAPIException, OperationNotSupported
+from ogr.services import gitlab as ogr_gitlab
+from ogr.services.base import BaseCommitFlag
+
+logger = logging.getLogger(__name__)
+
+
+class GitlabCommitFlag(BaseCommitFlag):
+    _states: ClassVar[dict[str, CommitStatus]] = {
+        "pending": CommitStatus.pending,
+        "success": CommitStatus.success,
+        "failed": CommitStatus.failure,
+        "canceled": CommitStatus.canceled,
+        "running": CommitStatus.running,
+    }
+
+    @staticmethod
+    def _state_from_enum(state: CommitStatus) -> str:
+        return "failed" if state == CommitStatus.failure else state.name
+
+    def __str__(self) -> str:
+        return (
+            f"GitlabCommitFlag("
+            f"commit='{self.commit}', "
+            f"state='{self.state.name}', "
+            f"context='{self.context}', "
+            f"uid='{self.uid}', "
+            f"comment='{self.comment}', "
+            f"url='{self.url}', "
+            f"created='{self.created}')"
+        )
+
+    def _from_raw_commit_flag(self):
+        self.commit = self._raw_commit_flag.sha
+        self.state = self._state_from_str(self._raw_commit_flag.status)
+        self.context = self._raw_commit_flag.name
+        self.comment = self._raw_commit_flag.description
+        self.uid = self._raw_commit_flag.id
+        self.url = self._raw_commit_flag.target_url
+
+    @staticmethod
+    def get(project: "ogr_gitlab.GitlabProject", commit: str) -> list["CommitFlag"]:
+        try:
+            commit_object = project.gitlab_repo.commits.get(commit)
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Commit {commit} was not found.")
+            raise GitlabAPIException(f"Commit {commit} was not found.") from ex
+
+        raw_statuses = commit_object.statuses.list(all=True)
+        return [
+            GitlabCommitFlag(raw_commit_flag=raw_status, project=project)
+            for raw_status in raw_statuses
+        ]
+
+    @staticmethod
+    def set(
+        project: "ogr_gitlab.GitlabProject",
+        commit: str,
+        state: CommitStatus,
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        state = GitlabCommitFlag._validate_state(state)
+
+        if trim:
+            description = description[:140]
+
+        try:
+            commit_object = project.gitlab_repo.commits.get(commit)
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Commit {commit} was not found.")
+            raise GitlabAPIException(f"Commit {commit} was not found.") from ex
+
+        data_dict = {
+            "state": GitlabCommitFlag._state_from_enum(state),
+            "target_url": target_url,
+            "context": context,
+            "description": description,
+        }
+        raw_status = commit_object.statuses.create(data_dict)
+        return GitlabCommitFlag(raw_commit_flag=raw_status, project=project)
+
+    @property
+    def created(self) -> datetime.datetime:
+        return datetime.datetime.strptime(
+            self._raw_commit_flag.created_at,
+            "%Y-%m-%dT%H:%M:%S.%fZ",
+        )
+
+    @property
+    def edited(self) -> datetime.datetime:
+        raise OperationNotSupported(
+            "GitLab doesn't support edited on commit flags, for more info "
+            "see https://github.com/packit/ogr/issues/413#issuecomment-729623702",
+        )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GitlabCommitFlag +(raw_commit_flag: Optional[Any] = None, project: Optional[ForwardRef('GitProject')] = None, commit: Optional[str] = None, state: Optional[CommitStatus] = None, context: Optional[str] = None, comment: Optional[str] = None, uid: Optional[str] = None, url: Optional[str] = None) +
+
+
+
+ +Expand source code + +
class GitlabCommitFlag(BaseCommitFlag):
+    _states: ClassVar[dict[str, CommitStatus]] = {
+        "pending": CommitStatus.pending,
+        "success": CommitStatus.success,
+        "failed": CommitStatus.failure,
+        "canceled": CommitStatus.canceled,
+        "running": CommitStatus.running,
+    }
+
+    @staticmethod
+    def _state_from_enum(state: CommitStatus) -> str:
+        return "failed" if state == CommitStatus.failure else state.name
+
+    def __str__(self) -> str:
+        return (
+            f"GitlabCommitFlag("
+            f"commit='{self.commit}', "
+            f"state='{self.state.name}', "
+            f"context='{self.context}', "
+            f"uid='{self.uid}', "
+            f"comment='{self.comment}', "
+            f"url='{self.url}', "
+            f"created='{self.created}')"
+        )
+
+    def _from_raw_commit_flag(self):
+        self.commit = self._raw_commit_flag.sha
+        self.state = self._state_from_str(self._raw_commit_flag.status)
+        self.context = self._raw_commit_flag.name
+        self.comment = self._raw_commit_flag.description
+        self.uid = self._raw_commit_flag.id
+        self.url = self._raw_commit_flag.target_url
+
+    @staticmethod
+    def get(project: "ogr_gitlab.GitlabProject", commit: str) -> list["CommitFlag"]:
+        try:
+            commit_object = project.gitlab_repo.commits.get(commit)
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Commit {commit} was not found.")
+            raise GitlabAPIException(f"Commit {commit} was not found.") from ex
+
+        raw_statuses = commit_object.statuses.list(all=True)
+        return [
+            GitlabCommitFlag(raw_commit_flag=raw_status, project=project)
+            for raw_status in raw_statuses
+        ]
+
+    @staticmethod
+    def set(
+        project: "ogr_gitlab.GitlabProject",
+        commit: str,
+        state: CommitStatus,
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        state = GitlabCommitFlag._validate_state(state)
+
+        if trim:
+            description = description[:140]
+
+        try:
+            commit_object = project.gitlab_repo.commits.get(commit)
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Commit {commit} was not found.")
+            raise GitlabAPIException(f"Commit {commit} was not found.") from ex
+
+        data_dict = {
+            "state": GitlabCommitFlag._state_from_enum(state),
+            "target_url": target_url,
+            "context": context,
+            "description": description,
+        }
+        raw_status = commit_object.statuses.create(data_dict)
+        return GitlabCommitFlag(raw_commit_flag=raw_status, project=project)
+
+    @property
+    def created(self) -> datetime.datetime:
+        return datetime.datetime.strptime(
+            self._raw_commit_flag.created_at,
+            "%Y-%m-%dT%H:%M:%S.%fZ",
+        )
+
+    @property
+    def edited(self) -> datetime.datetime:
+        raise OperationNotSupported(
+            "GitLab doesn't support edited on commit flags, for more info "
+            "see https://github.com/packit/ogr/issues/413#issuecomment-729623702",
+        )
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/gitlab/index.html b/docs/services/gitlab/index.html new file mode 100644 index 00000000..d8f9b443 --- /dev/null +++ b/docs/services/gitlab/index.html @@ -0,0 +1,2072 @@ + + + + + + +ogr.services.gitlab API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.gitlab

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from ogr.services.gitlab.comments import GitlabIssueComment, GitlabPRComment
+from ogr.services.gitlab.issue import GitlabIssue
+from ogr.services.gitlab.project import GitlabProject
+from ogr.services.gitlab.pull_request import GitlabPullRequest
+from ogr.services.gitlab.release import GitlabRelease
+from ogr.services.gitlab.service import GitlabService
+from ogr.services.gitlab.user import GitlabUser
+
+__all__ = [
+    GitlabIssue.__name__,
+    GitlabPullRequest.__name__,
+    GitlabIssueComment.__name__,
+    GitlabPRComment.__name__,
+    GitlabRelease.__name__,
+    GitlabUser.__name__,
+    GitlabProject.__name__,
+    GitlabService.__name__,
+]
+
+
+
+

Sub-modules

+
+
ogr.services.gitlab.comments
+
+
+
+
ogr.services.gitlab.flag
+
+
+
+
ogr.services.gitlab.issue
+
+
+
+
ogr.services.gitlab.project
+
+
+
+
ogr.services.gitlab.pull_request
+
+
+
+
ogr.services.gitlab.release
+
+
+
+
ogr.services.gitlab.service
+
+
+
+
ogr.services.gitlab.user
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GitlabIssue +(raw_issue: Any, project: GitProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the issue.
+
+
+ +Expand source code + +
class GitlabIssue(BaseIssue):
+    _raw_issue: _GitlabIssue
+
+    @property
+    def title(self) -> str:
+        return self._raw_issue.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_issue.title = new_title
+        self._raw_issue.save()
+
+    @property
+    def id(self) -> int:
+        return self._raw_issue.iid
+
+    @property
+    def private(self) -> bool:
+        return self._raw_issue.confidential
+
+    @property
+    def status(self) -> IssueStatus:
+        return (
+            IssueStatus.open
+            if self._raw_issue.state == "opened"
+            else IssueStatus[self._raw_issue.state]
+        )
+
+    @property
+    def url(self) -> str:
+        return self._raw_issue.web_url
+
+    @property
+    def assignees(self) -> list:
+        try:
+            return self._raw_issue.assignees
+        except AttributeError:
+            return None  # if issue has no assignees, the attribute is not present
+
+    @property
+    def description(self) -> str:
+        return self._raw_issue.description
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_issue.description = new_description
+        self._raw_issue.save()
+
+    @property
+    def author(self) -> str:
+        return self._raw_issue.author["username"]
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_issue.created_at
+
+    @property
+    def labels(self) -> list:
+        return self._raw_issue.labels
+
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_gitlab.GitlabProject",
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        assignee_ids = []
+        for user in assignees or []:
+            users_list = project.service.gitlab_instance.users.list(username=user)
+
+            if not users_list:
+                raise GitlabAPIException(f"Unable to find '{user}' username")
+
+            assignee_ids.append(str(users_list[0].id))
+
+        data = {"title": title, "description": body}
+        if labels:
+            data["labels"] = ",".join(labels)
+        if assignees:
+            data["assignee_ids"] = ",".join(assignee_ids)
+
+        issue = project.gitlab_repo.issues.create(data, confidential=private)
+        return GitlabIssue(issue, project)
+
+    @staticmethod
+    def get(project: "ogr_gitlab.GitlabProject", issue_id: int) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        try:
+            return GitlabIssue(project.gitlab_repo.issues.get(issue_id), project)
+        except gitlab.exceptions.GitlabGetError as ex:
+            raise GitlabAPIException(f"Issue {issue_id} was not found. ") from ex
+
+    @staticmethod
+    def get_list(
+        project: "ogr_gitlab.GitlabProject",
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        # Gitlab API has status 'opened', not 'open'
+        parameters: dict[str, Union[str, list[str], bool]] = {
+            "state": status.name if status != IssueStatus.open else "opened",
+            "order_by": "updated_at",
+            "sort": "desc",
+            "all": True,
+        }
+        if author:
+            parameters["author_username"] = author
+        if assignee:
+            parameters["assignee_username"] = assignee
+        if labels:
+            parameters["labels"] = labels
+
+        issues = project.gitlab_repo.issues.list(**parameters)
+        return [GitlabIssue(issue, project) for issue in issues]
+
+    def _get_all_comments(self) -> list[IssueComment]:
+        return [
+            GitlabIssueComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_issue.notes.list(sort="asc", all=True)
+        ]
+
+    def comment(self, body: str) -> IssueComment:
+        comment = self._raw_issue.notes.create({"body": body})
+        return GitlabIssueComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "Issue":
+        self._raw_issue.state_event = "close"
+        self._raw_issue.save()
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        for label in labels:
+            self._raw_issue.labels.append(label)
+        self._raw_issue.save()
+
+    def add_assignee(self, *assignees: str) -> None:
+        assignee_ids = self._raw_issue.__dict__.get("assignee_ids") or []
+        for assignee in assignees:
+            users = self.project.service.gitlab_instance.users.list(  # type: ignore
+                username=assignee,
+            )
+            if not users:
+                raise GitlabAPIException(f"Unable to find '{assignee}' username")
+            uid = str(users[0].id)
+            if uid not in assignee_ids:
+                assignee_ids.append(str(users[0].id))
+
+        self._raw_issue.assignee_ids = assignee_ids
+        self._raw_issue.save()
+
+    def get_comment(self, comment_id: int) -> IssueComment:
+        return GitlabIssueComment(self._raw_issue.notes.get(comment_id))
+
+

Ancestors

+ +

Instance variables

+
+
var assignees : list
+
+
+
+ +Expand source code + +
@property
+def assignees(self) -> list:
+    try:
+        return self._raw_issue.assignees
+    except AttributeError:
+        return None  # if issue has no assignees, the attribute is not present
+
+
+
+

Inherited members

+ +
+
+class GitlabIssueComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class GitlabIssueComment(GitlabComment, IssueComment):
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+class GitlabPRComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class GitlabPRComment(GitlabComment, PRComment):
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+class GitlabProject +(repo: str, service: ogr_gitlab.GitlabService, namespace: str, gitlab_repo=None, **unprocess_kwargs) +
+
+

Args

+
+
repo
+
Name of the project.
+
service
+
GitService instance.
+
namespace
+
+

Namespace of the project.

+
    +
  • GitHub: username or org name.
  • +
  • GitLab: username or org name.
  • +
  • Pagure: namespace (e.g. "rpms").
  • +
+

In case of forks: "fork/{username}/{namespace}".

+
+
+
+ +Expand source code + +
class GitlabProject(BaseGitProject):
+    service: "ogr_gitlab.GitlabService"
+
+    def __init__(
+        self,
+        repo: str,
+        service: "ogr_gitlab.GitlabService",
+        namespace: str,
+        gitlab_repo=None,
+        **unprocess_kwargs,
+    ) -> None:
+        if unprocess_kwargs:
+            logger.warning(
+                f"GitlabProject will not process these kwargs: {unprocess_kwargs}",
+            )
+        super().__init__(repo, service, namespace)
+        self._gitlab_repo = gitlab_repo
+        self.read_only = False
+
+    @property
+    def gitlab_repo(self) -> GitlabObjectsProject:
+        if not self._gitlab_repo:
+            self._gitlab_repo = self.service.gitlab_instance.projects.get(
+                f"{self.namespace}/{self.repo}",
+            )
+        return self._gitlab_repo
+
+    @property
+    def is_fork(self) -> bool:
+        return bool("forked_from_project" in self.gitlab_repo.attributes)
+
+    @property
+    def parent(self) -> Optional["GitlabProject"]:
+        if self.is_fork:
+            parent_dict = self.gitlab_repo.attributes["forked_from_project"]
+            return GitlabProject(
+                repo=parent_dict["path"],
+                service=self.service,
+                namespace=parent_dict["namespace"]["full_path"],
+            )
+        return None
+
+    @property
+    def default_branch(self) -> Optional[str]:
+        return self.gitlab_repo.attributes.get("default_branch")
+
+    def __str__(self) -> str:
+        return f'GitlabProject(namespace="{self.namespace}", repo="{self.repo}")'
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, GitlabProject):
+            return False
+
+        return (
+            self.repo == o.repo
+            and self.namespace == o.namespace
+            and self.service == o.service
+        )
+
+    @property
+    def has_issues(self) -> bool:
+        return self.gitlab_repo.issues_enabled
+
+    def _construct_fork_project(self) -> Optional["GitlabProject"]:
+        user_login = self.service.user.get_username()
+        try:
+            project = GitlabProject(
+                repo=self.repo,
+                service=self.service,
+                namespace=user_login,
+            )
+            if project.gitlab_repo:
+                return project
+        except Exception as ex:
+            logger.debug(f"Project {user_login}/{self.repo} does not exist: {ex}")
+        return None
+
+    def exists(self) -> bool:
+        try:
+            _ = self.gitlab_repo
+            return True
+        except gitlab.exceptions.GitlabGetError as ex:
+            if "404 Project Not Found" in str(ex):
+                return False
+            raise GitlabAPIException from ex
+
+    def is_private(self) -> bool:
+        return self.gitlab_repo.attributes["visibility"] == "private"
+
+    def is_forked(self) -> bool:
+        return bool(self._construct_fork_project())
+
+    def get_description(self) -> str:
+        return self.gitlab_repo.attributes["description"]
+
+    @property
+    def description(self) -> str:
+        return self.gitlab_repo.attributes["description"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.gitlab_repo.description = new_description
+        self.gitlab_repo.save()
+
+    def get_fork(self, create: bool = True) -> Optional["GitlabProject"]:
+        username = self.service.user.get_username()
+        for fork in self.get_forks():
+            if fork.gitlab_repo.namespace["full_path"] == username:
+                return fork
+
+        if not self.is_forked():
+            if create:
+                return self.fork_create()
+
+            logger.info(
+                f"Fork of {self.gitlab_repo.attributes['name']}"
+                " does not exist and we were asked not to create it.",
+            )
+            return None
+        return self._construct_fork_project()
+
+    def get_owners(self) -> list[str]:
+        return self._get_collaborators_with_given_access(
+            access_levels=[gitlab.const.OWNER_ACCESS],
+        )
+
+    def who_can_close_issue(self) -> set[str]:
+        return set(
+            self._get_collaborators_with_given_access(
+                access_levels=[
+                    gitlab.const.REPORTER_ACCESS,
+                    gitlab.const.DEVELOPER_ACCESS,
+                    gitlab.const.MAINTAINER_ACCESS,
+                    gitlab.const.OWNER_ACCESS,
+                ],
+            ),
+        )
+
+    def who_can_merge_pr(self) -> set[str]:
+        return set(
+            self._get_collaborators_with_given_access(
+                access_levels=[
+                    gitlab.const.DEVELOPER_ACCESS,
+                    gitlab.const.MAINTAINER_ACCESS,
+                    gitlab.const.OWNER_ACCESS,
+                ],
+            ),
+        )
+
+    def can_merge_pr(self, username) -> bool:
+        return username in self.who_can_merge_pr()
+
+    def delete(self) -> None:
+        self.gitlab_repo.delete()
+
+    def _get_collaborators_with_given_access(
+        self,
+        access_levels: list[int],
+    ) -> list[str]:
+        """
+        Get all project collaborators with one of the given access levels.
+        Access levels:
+            10 => Guest access
+            20 => Reporter access
+            30 => Developer access
+            40 => Maintainer access
+            50 => Owner access
+
+        Returns:
+            List of usernames.
+        """
+        # TODO: Remove once ‹members_all› is available for all releases of ogr
+        all_members = None
+        if hasattr(self.gitlab_repo, "members_all"):
+            all_members = self.gitlab_repo.members_all.list(all=True)
+        else:
+            all_members = self.gitlab_repo.members.all(all=True)
+
+        response = []
+        for member in all_members:
+            if isinstance(member, dict):
+                access_level = member["access_level"]
+                username = member["username"]
+            else:
+                access_level = member.access_level
+                username = member.username
+            if access_level in access_levels:
+                response.append(username)
+        return response
+
+    def add_user(self, user: str, access_level: AccessLevel) -> None:
+        access_dict = {
+            AccessLevel.pull: gitlab.const.GUEST_ACCESS,
+            AccessLevel.triage: gitlab.const.REPORTER_ACCESS,
+            AccessLevel.push: gitlab.const.DEVELOPER_ACCESS,
+            AccessLevel.admin: gitlab.const.MAINTAINER_ACCESS,
+            AccessLevel.maintain: gitlab.const.OWNER_ACCESS,
+        }
+        try:
+            user_id = self.service.gitlab_instance.users.list(username=user)[0].id
+        except Exception as e:
+            raise GitlabAPIException(f"User {user} not found") from e
+        try:
+            self.gitlab_repo.members.create(
+                {"user_id": user_id, "access_level": access_dict[access_level]},
+            )
+        except Exception as e:
+            raise GitlabAPIException(f"User {user} already exists") from e
+
+    def request_access(self) -> None:
+        try:
+            self.gitlab_repo.accessrequests.create({})
+        except gitlab.exceptions.GitlabCreateError as e:
+            raise GitlabAPIException("Unable to request access") from e
+
+    @indirect(GitlabPullRequest.get_list)
+    def get_pr_list(self, status: PRStatus = PRStatus.open) -> list["PullRequest"]:
+        pass
+
+    def get_sha_from_tag(self, tag_name: str) -> str:
+        try:
+            tag = self.gitlab_repo.tags.get(tag_name)
+            return tag.attributes["commit"]["id"]
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Tag {tag_name} was not found.")
+            raise GitlabAPIException(f"Tag {tag_name} was not found.") from ex
+
+    @indirect(GitlabPullRequest.create)
+    def create_pr(
+        self,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        pass
+
+    def commit_comment(
+        self,
+        commit: str,
+        body: str,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "CommitComment":
+        try:
+            commit_object: ProjectCommit = self.gitlab_repo.commits.get(commit)
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Commit {commit} was not found.")
+            raise GitlabAPIException(f"Commit {commit} was not found.") from ex
+
+        if filename and row:
+            raw_comment = commit_object.comments.create(
+                {"note": body, "path": filename, "line": row, "line_type": "new"},
+            )
+        else:
+            raw_comment = commit_object.comments.create({"note": body})
+        return self._commit_comment_from_gitlab_object(raw_comment, commit)
+
+    @staticmethod
+    def _commit_comment_from_gitlab_object(raw_comment, commit) -> CommitComment:
+        return CommitComment(
+            sha=commit,
+            body=raw_comment.note,
+            author=raw_comment.author["username"],
+        )
+
+    def get_commit_comments(self, commit: str) -> list[CommitComment]:
+        try:
+            commit_object: ProjectCommit = self.gitlab_repo.commits.get(commit)
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Commit {commit} was not found.")
+            raise GitlabAPIException(f"Commit {commit} was not found.") from ex
+
+        return [
+            self._commit_comment_from_gitlab_object(comment, commit)
+            for comment in commit_object.comments.list()
+        ]
+
+    @indirect(GitlabCommitFlag.set)
+    def set_commit_status(
+        self,
+        commit: str,
+        state: Union[CommitStatus, str],
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        pass
+
+    @indirect(GitlabCommitFlag.get)
+    def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+        pass
+
+    def get_git_urls(self) -> dict[str, str]:
+        return {
+            "git": self.gitlab_repo.attributes["http_url_to_repo"],
+            "ssh": self.gitlab_repo.attributes["ssh_url_to_repo"],
+        }
+
+    def fork_create(self, namespace: Optional[str] = None) -> "GitlabProject":
+        data = {}
+        if namespace:
+            data["namespace_path"] = namespace
+
+        try:
+            fork = self.gitlab_repo.forks.create(data=data)
+        except gitlab.GitlabCreateError as ex:
+            logger.error(f"Repo {self.gitlab_repo} cannot be forked")
+            raise GitlabAPIException(
+                f"Repo {self.gitlab_repo} cannot be forked",
+            ) from ex
+        logger.debug(f"Forked to {fork.namespace['full_path']}/{fork.path}")
+        return GitlabProject(
+            namespace=fork.namespace["full_path"],
+            service=self.service,
+            repo=fork.path,
+        )
+
+    def change_token(self, new_token: str):
+        self.service.change_token(new_token)
+
+    def get_branches(self) -> list[str]:
+        return [branch.name for branch in self.gitlab_repo.branches.list(all=True)]
+
+    def get_file_content(self, path, ref=None) -> str:
+        ref = ref or self.default_branch
+        try:
+            file = self.gitlab_repo.files.get(file_path=path, ref=ref)
+            return file.decode().decode()
+        except gitlab.exceptions.GitlabGetError as ex:
+            if ex.response_code == 404:
+                raise FileNotFoundError(f"File '{path}' on {ref} not found") from ex
+            raise GitlabAPIException() from ex
+
+    def get_files(
+        self,
+        ref: Optional[str] = None,
+        filter_regex: Optional[str] = None,
+        recursive: bool = False,
+    ) -> list[str]:
+        ref = ref or self.default_branch
+        paths = [
+            file_dict["path"]
+            for file_dict in self.gitlab_repo.repository_tree(
+                ref=ref,
+                recursive=recursive,
+                all=True,
+            )
+            if file_dict["type"] != "tree"
+        ]
+        if filter_regex:
+            paths = filter_paths(paths, filter_regex)
+
+        return paths
+
+    @indirect(GitlabIssue.get_list)
+    def get_issue_list(
+        self,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list[Issue]:
+        pass
+
+    @indirect(GitlabIssue.get)
+    def get_issue(self, issue_id: int) -> Issue:
+        pass
+
+    @indirect(GitlabIssue.create)
+    def create_issue(
+        self,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> Issue:
+        pass
+
+    @indirect(GitlabPullRequest.get)
+    def get_pr(self, pr_id: int) -> PullRequest:
+        pass
+
+    def get_tags(self) -> list["GitTag"]:
+        tags = self.gitlab_repo.tags.list()
+        return [GitTag(tag.name, tag.commit["id"]) for tag in tags]
+
+    def _git_tag_from_tag_name(self, tag_name: str) -> GitTag:
+        git_tag = self.gitlab_repo.tags.get(tag_name)
+        return GitTag(name=git_tag.name, commit_sha=git_tag.commit["id"])
+
+    @indirect(GitlabRelease.get_list)
+    def get_releases(self) -> list[Release]:
+        pass
+
+    @indirect(GitlabRelease.get)
+    def get_release(self, identifier=None, name=None, tag_name=None) -> GitlabRelease:
+        pass
+
+    @indirect(GitlabRelease.create)
+    def create_release(
+        self,
+        tag: str,
+        name: str,
+        message: str,
+        commit_sha: Optional[str] = None,
+    ) -> GitlabRelease:
+        pass
+
+    @indirect(GitlabRelease.get_latest)
+    def get_latest_release(self) -> Optional[GitlabRelease]:
+        pass
+
+    def list_labels(self):
+        """
+        Get list of labels in the repository.
+
+        Returns:
+            List of labels in the repository.
+        """
+        return list(self.gitlab_repo.labels.list())
+
+    def get_forks(self) -> list["GitlabProject"]:
+        try:
+            forks = self.gitlab_repo.forks.list()
+        except KeyError as ex:
+            # > item = self._data[self._current]
+            # > KeyError: 0
+            # looks like some API weirdness
+            raise OperationNotSupported(
+                "Please upgrade python-gitlab to a newer version.",
+            ) from ex
+        return [
+            GitlabProject(
+                repo=fork.path,
+                namespace=fork.namespace["full_path"],
+                service=self.service,
+            )
+            for fork in forks
+        ]
+
+    def update_labels(self, labels):
+        """
+        Update the labels of the repository. (No deletion, only add not existing ones.)
+
+        Args:
+            labels: List of labels to be added.
+
+        Returns:
+            Number of added labels.
+        """
+        current_label_names = [la.name for la in list(self.gitlab_repo.labels.list())]
+        changes = 0
+        for label in labels:
+            if label.name not in current_label_names:
+                color = self._normalize_label_color(color=label.color)
+                self.gitlab_repo.labels.create(
+                    {
+                        "name": label.name,
+                        "color": color,
+                        "description": label.description or "",
+                    },
+                )
+
+                changes += 1
+        return changes
+
+    @staticmethod
+    def _normalize_label_color(color):
+        if not color.startswith("#"):
+            return f"#{color}"
+        return color
+
+    def get_web_url(self) -> str:
+        return self.gitlab_repo.web_url
+
+    def get_sha_from_branch(self, branch: str) -> Optional[str]:
+        try:
+            return self.gitlab_repo.branches.get(branch).attributes["commit"]["id"]
+        except GitlabGetError as ex:
+            if ex.response_code == 404:
+                return None
+            raise GitlabAPIException from ex
+
+    def get_contributors(self) -> set[str]:
+        """
+        Returns:
+            Unique authors of the commits in the project.
+        """
+
+        def format_contributor(contributor: dict[str, Any]) -> str:
+            return f"{contributor['name']} <{contributor['email']}>"
+
+        return set(
+            map(format_contributor, self.gitlab_repo.repository_contributors(all=True)),
+        )
+
+    def users_with_write_access(self) -> set[str]:
+        return set(
+            self._get_collaborators_with_given_access(
+                access_levels=[
+                    gitlab.const.DEVELOPER_ACCESS,
+                    gitlab.const.MAINTAINER_ACCESS,
+                    gitlab.const.OWNER_ACCESS,
+                ],
+            ),
+        )
+
+

Ancestors

+ +

Class variables

+
+
var serviceGitlabService
+
+
+
+
+

Instance variables

+
+
var gitlab_repo : gitlab.v4.objects.projects.Project
+
+
+
+ +Expand source code + +
@property
+def gitlab_repo(self) -> GitlabObjectsProject:
+    if not self._gitlab_repo:
+        self._gitlab_repo = self.service.gitlab_instance.projects.get(
+            f"{self.namespace}/{self.repo}",
+        )
+    return self._gitlab_repo
+
+
+
+

Methods

+
+
+def get_contributors(self) ‑> set[str] +
+
+

Returns

+

Unique authors of the commits in the project.

+
+ +Expand source code + +
def get_contributors(self) -> set[str]:
+    """
+    Returns:
+        Unique authors of the commits in the project.
+    """
+
+    def format_contributor(contributor: dict[str, Any]) -> str:
+        return f"{contributor['name']} <{contributor['email']}>"
+
+    return set(
+        map(format_contributor, self.gitlab_repo.repository_contributors(all=True)),
+    )
+
+
+
+def list_labels(self) +
+
+

Get list of labels in the repository.

+

Returns

+

List of labels in the repository.

+
+ +Expand source code + +
def list_labels(self):
+    """
+    Get list of labels in the repository.
+
+    Returns:
+        List of labels in the repository.
+    """
+    return list(self.gitlab_repo.labels.list())
+
+
+
+def update_labels(self, labels) +
+
+

Update the labels of the repository. (No deletion, only add not existing ones.)

+

Args

+
+
labels
+
List of labels to be added.
+
+

Returns

+

Number of added labels.

+
+ +Expand source code + +
def update_labels(self, labels):
+    """
+    Update the labels of the repository. (No deletion, only add not existing ones.)
+
+    Args:
+        labels: List of labels to be added.
+
+    Returns:
+        Number of added labels.
+    """
+    current_label_names = [la.name for la in list(self.gitlab_repo.labels.list())]
+    changes = 0
+    for label in labels:
+        if label.name not in current_label_names:
+            color = self._normalize_label_color(color=label.color)
+            self.gitlab_repo.labels.create(
+                {
+                    "name": label.name,
+                    "color": color,
+                    "description": label.description or "",
+                },
+            )
+
+            changes += 1
+    return changes
+
+
+
+

Inherited members

+ +
+
+class GitlabPullRequest +(raw_pr: Any, project: GitProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the pull request.
+
+
+ +Expand source code + +
class GitlabPullRequest(BasePullRequest):
+    _raw_pr: _GitlabMergeRequest
+    _target_project: "ogr_gitlab.GitlabProject"
+    _source_project: "ogr_gitlab.GitlabProject" = None
+    _merge_commit_status: ClassVar[dict[str, MergeCommitStatus]] = {
+        "can_be_merged": MergeCommitStatus.can_be_merged,
+        "cannot_be_merged": MergeCommitStatus.cannot_be_merged,
+        "unchecked": MergeCommitStatus.unchecked,
+        "checking": MergeCommitStatus.checking,
+        "cannot_be_merged_recheck": MergeCommitStatus.cannot_be_merged_recheck,
+    }
+
+    @property
+    def title(self) -> str:
+        return self._raw_pr.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_pr.title = new_title
+        self._raw_pr.save()
+
+    @property
+    def id(self) -> int:
+        return self._raw_pr.iid
+
+    @property
+    def status(self) -> PRStatus:
+        return (
+            PRStatus.open
+            if self._raw_pr.state == "opened"
+            else PRStatus[self._raw_pr.state]
+        )
+
+    @property
+    def url(self) -> str:
+        return self._raw_pr.web_url
+
+    @property
+    def description(self) -> str:
+        return self._raw_pr.description
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_pr.description = new_description
+        self._raw_pr.save()
+
+    @property
+    def author(self) -> str:
+        return self._raw_pr.author["username"]
+
+    @property
+    def source_branch(self) -> str:
+        return self._raw_pr.source_branch
+
+    @property
+    def target_branch(self) -> str:
+        return self._raw_pr.target_branch
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_pr.created_at
+
+    @property
+    def labels(self) -> list[str]:
+        return self._raw_pr.labels
+
+    @property
+    def diff_url(self) -> str:
+        return f"{self._raw_pr.web_url}/diffs"
+
+    @property
+    def commits_url(self) -> str:
+        return f"{self._raw_pr.web_url}/commits"
+
+    @property
+    def patch(self) -> bytes:
+        response = requests.get(f"{self.url}.patch")
+
+        if not response.ok:
+            cls = OgrNetworkError if response.status_code >= 500 else GitlabAPIException
+            raise cls(
+                f"Couldn't get patch from {self.url}.patch because {response.reason}.",
+            )
+
+        return response.content
+
+    @property
+    def head_commit(self) -> str:
+        return self._raw_pr.sha
+
+    @property
+    def merge_commit_sha(self) -> Optional[str]:
+        # when merged => return merge_commit_sha
+        # otherwise => return test merge if possible
+        if self.status == PRStatus.merged:
+            return self._raw_pr.merge_commit_sha
+
+        # works for test merge only with python-gitlab>=2.10.0
+        try:
+            response = self._raw_pr.merge_ref()
+        except GitlabGetError as ex:
+            if ex.response_code == 400:
+                return None
+            raise
+        return response.get("commit_id")
+
+    @property
+    def merge_commit_status(self) -> MergeCommitStatus:
+        status = self._raw_pr.merge_status
+        if status not in self._merge_commit_status:
+            raise GitlabAPIException(f"Invalid merge_status {status}")
+
+        return self._merge_commit_status[status]
+
+    @property
+    def source_project(self) -> "ogr_gitlab.GitlabProject":
+        if self._source_project is None:
+            self._source_project = (
+                self._target_project.service.get_project_from_project_id(
+                    self._raw_pr.attributes["source_project_id"],
+                )
+            )
+        return self._source_project
+
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_gitlab.GitlabProject",
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        How to create PR:
+        -  upstream -> upstream - call on upstream, fork_username unset
+        -  fork -> upstream - call on fork, fork_username unset
+           also can call on upstream with fork_username, not supported way of using
+        -  fork -> fork - call on fork, fork_username set
+        -  fork -> other_fork - call on fork, fork_username set to other_fork owner
+        """
+        repo = project.gitlab_repo
+        parameters = {
+            "source_branch": source_branch,
+            "target_branch": target_branch,
+            "title": title,
+            "description": body,
+        }
+        target_id = None
+
+        target_project = project
+        if project.is_fork and fork_username is None:
+            # handles fork -> upstream (called on fork)
+            target_id = project.parent.gitlab_repo.attributes["id"]
+            target_project = project.parent
+        elif fork_username and fork_username != project.namespace:
+            # handles fork -> upstream
+            #   (username of fork owner specified by fork_username)
+            # handles fork -> other_fork
+            #   (username of other_fork owner specified by fork_username)
+
+            other_project = GitlabPullRequest.__get_fork(
+                fork_username,
+                project if project.parent is None else project.parent,
+            )
+
+            target_id = other_project.gitlab_repo.attributes["id"]
+            if project.parent is None:
+                target_id = repo.attributes["id"]
+                repo = other_project.gitlab_repo
+        # otherwise handles PR from the same project to same project
+
+        if target_id is not None:
+            parameters["target_project_id"] = target_id
+
+        mr = repo.mergerequests.create(parameters)
+        return GitlabPullRequest(mr, target_project)
+
+    @staticmethod
+    def __get_fork(
+        fork_username: str,
+        project: "ogr_gitlab.GitlabProject",
+    ) -> "ogr_gitlab.GitlabProject":
+        """
+        Returns forked project of a requested user. Internal method, in case the fork
+        doesn't exist, raises GitlabAPIException.
+
+        Args:
+            fork_username: Username of a user that owns requested fork.
+            project: Project to search forks of.
+
+        Returns:
+            Requested fork.
+
+        Raises:
+            GitlabAPIException, in case the fork doesn't exist.
+        """
+        forks = list(
+            filter(
+                lambda fork: fork.gitlab_repo.namespace["full_path"] == fork_username,
+                project.get_forks(),
+            ),
+        )
+        if not forks:
+            raise GitlabAPIException("Requested fork doesn't exist")
+        return forks[0]
+
+    @staticmethod
+    def get(project: "ogr_gitlab.GitlabProject", pr_id: int) -> "PullRequest":
+        try:
+            mr = project.gitlab_repo.mergerequests.get(pr_id)
+        except gitlab.GitlabGetError as ex:
+            raise GitlabAPIException(f"No PR with id {pr_id} found") from ex
+        return GitlabPullRequest(mr, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_gitlab.GitlabProject",
+        status: PRStatus = PRStatus.open,
+    ) -> list["PullRequest"]:
+        # Gitlab API has status 'opened', not 'open'
+        # f"Calling a `list()` method without specifying `get_all=True` or "
+        # f"`iterator=True` will return a maximum of 20 items. "
+        mrs = project.gitlab_repo.mergerequests.list(
+            state=status.name if status != PRStatus.open else "opened",
+            order_by="updated_at",
+            sort="desc",
+            # gitlab 3.3 syntax was all=True
+            get_all=True,
+        )
+        return [GitlabPullRequest(mr, project) for mr in mrs]
+
+    def update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "PullRequest":
+        if title:
+            self._raw_pr.title = title
+        if description:
+            self._raw_pr.description = description
+
+        self._raw_pr.save()
+        return self
+
+    def _get_all_comments(self) -> list[PRComment]:
+        return [
+            GitlabPRComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_pr.notes.list(sort="asc", all=True)
+        ]
+
+    def get_all_commits(self) -> list[str]:
+        return [commit.id for commit in self._raw_pr.commits()]
+
+    def comment(
+        self,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        comment = self._raw_pr.notes.create({"body": body})
+        return GitlabPRComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "PullRequest":
+        self._raw_pr.state_event = "close"
+        self._raw_pr.save()
+        return self
+
+    def merge(self) -> "PullRequest":
+        self._raw_pr.merge()
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        self._raw_pr.labels += labels
+        self._raw_pr.save()
+
+    def get_comment(self, comment_id: int) -> PRComment:
+        return GitlabPRComment(self._raw_pr.notes.get(comment_id))
+
+

Ancestors

+ +

Static methods

+
+
+def create(project: ogr_gitlab.GitlabProject, title: str, body: str, target_branch: str, source_branch: str, fork_username: Optional[str] = None) ‑> PullRequest +
+
+

How to create PR: +- +upstream -> upstream - call on upstream, fork_username unset +- +fork -> upstream - call on fork, fork_username unset +also can call on upstream with fork_username, not supported way of using +- +fork -> fork - call on fork, fork_username set +- +fork -> other_fork - call on fork, fork_username set to other_fork owner

+
+ +Expand source code + +
@staticmethod
+def create(
+    project: "ogr_gitlab.GitlabProject",
+    title: str,
+    body: str,
+    target_branch: str,
+    source_branch: str,
+    fork_username: Optional[str] = None,
+) -> "PullRequest":
+    """
+    How to create PR:
+    -  upstream -> upstream - call on upstream, fork_username unset
+    -  fork -> upstream - call on fork, fork_username unset
+       also can call on upstream with fork_username, not supported way of using
+    -  fork -> fork - call on fork, fork_username set
+    -  fork -> other_fork - call on fork, fork_username set to other_fork owner
+    """
+    repo = project.gitlab_repo
+    parameters = {
+        "source_branch": source_branch,
+        "target_branch": target_branch,
+        "title": title,
+        "description": body,
+    }
+    target_id = None
+
+    target_project = project
+    if project.is_fork and fork_username is None:
+        # handles fork -> upstream (called on fork)
+        target_id = project.parent.gitlab_repo.attributes["id"]
+        target_project = project.parent
+    elif fork_username and fork_username != project.namespace:
+        # handles fork -> upstream
+        #   (username of fork owner specified by fork_username)
+        # handles fork -> other_fork
+        #   (username of other_fork owner specified by fork_username)
+
+        other_project = GitlabPullRequest.__get_fork(
+            fork_username,
+            project if project.parent is None else project.parent,
+        )
+
+        target_id = other_project.gitlab_repo.attributes["id"]
+        if project.parent is None:
+            target_id = repo.attributes["id"]
+            repo = other_project.gitlab_repo
+    # otherwise handles PR from the same project to same project
+
+    if target_id is not None:
+        parameters["target_project_id"] = target_id
+
+    mr = repo.mergerequests.create(parameters)
+    return GitlabPullRequest(mr, target_project)
+
+
+
+

Inherited members

+ +
+
+class GitlabRelease +(raw_release: Any, project: GitProject) +
+
+

Object that represents release.

+

Attributes

+
+
project : GitProject
+
Project on which the release is created.
+
+
+ +Expand source code + +
class GitlabRelease(Release):
+    _raw_release: _GitlabRelease
+    project: "ogr_gitlab.GitlabProject"
+
+    @property
+    def title(self):
+        return self._raw_release.name
+
+    @property
+    def body(self):
+        return self._raw_release.description
+
+    @property
+    def git_tag(self) -> GitTag:
+        return self.project._git_tag_from_tag_name(self.tag_name)
+
+    @property
+    def tag_name(self) -> str:
+        return self._raw_release.tag_name
+
+    @property
+    def url(self) -> Optional[str]:
+        return f"{self.project.get_web_url()}/-/releases/{self.tag_name}"
+
+    @property
+    def created_at(self) -> datetime.datetime:
+        return self._raw_release.created_at
+
+    @property
+    def tarball_url(self) -> str:
+        return self._raw_release.assets["sources"][1]["url"]
+
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+    @staticmethod
+    def get(
+        project: "ogr_gitlab.GitlabProject",
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> "Release":
+        release = project.gitlab_repo.releases.get(tag_name)
+        return GitlabRelease(release, project)
+
+    @staticmethod
+    def get_latest(project: "ogr_gitlab.GitlabProject") -> Optional["Release"]:
+        releases = project.gitlab_repo.releases.list()
+        # list of releases sorted by released_at
+        return GitlabRelease(releases[0], project) if releases else None
+
+    @staticmethod
+    def get_list(project: "ogr_gitlab.GitlabProject") -> list["Release"]:
+        if not hasattr(project.gitlab_repo, "releases"):
+            raise OperationNotSupported(
+                "This version of python-gitlab does not support release, please upgrade.",
+            )
+        releases = project.gitlab_repo.releases.list(all=True)
+        return [GitlabRelease(release, project) for release in releases]
+
+    @staticmethod
+    def create(
+        project: "ogr_gitlab.GitlabProject",
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> "Release":
+        release = project.gitlab_repo.releases.create(
+            {"name": name, "tag_name": tag, "description": message, "ref": ref},
+        )
+        return GitlabRelease(release, project)
+
+    def edit_release(self, name: str, message: str) -> None:
+        raise OperationNotSupported("edit_release not supported on GitLab")
+
+

Ancestors

+ +

Class variables

+
+
var projectGitlabProject
+
+
+
+
+

Inherited members

+ +
+
+class GitlabService +(token=None, instance_url=None, ssl_verify=True, **kwargs) +
+
+

Attributes

+
+
instance_url : str
+
URL of the git forge instance.
+
+
+ +Expand source code + +
@use_for_service("gitlab")  # anything containing a gitlab word in hostname
+# + list of community-hosted instances based on the following list
+# https://wiki.p2pfoundation.net/List_of_Community-Hosted_GitLab_Instances
+@use_for_service("salsa.debian.org")
+@use_for_service("git.fosscommunity.in")
+@use_for_service("framagit.org")
+@use_for_service("dev.gajim.org")
+@use_for_service("git.coop")
+@use_for_service("lab.libreho.st")
+@use_for_service("git.linux-kernel.at")
+@use_for_service("git.pleroma.social")
+@use_for_service("git.silence.dev")
+@use_for_service("code.videolan.org")
+@use_for_service("source.puri.sm")
+class GitlabService(BaseGitService):
+    name = "gitlab"
+
+    def __init__(self, token=None, instance_url=None, ssl_verify=True, **kwargs):
+        super().__init__(token=token)
+        self.instance_url = instance_url or "https://gitlab.com"
+        self.token = token
+        self.ssl_verify = ssl_verify
+        self._gitlab_instance = None
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    @property
+    def gitlab_instance(self) -> gitlab.Gitlab:
+        if not self._gitlab_instance:
+            self._gitlab_instance = gitlab.Gitlab(
+                url=self.instance_url,
+                private_token=self.token,
+                ssl_verify=self.ssl_verify,
+            )
+            if self.token:
+                self._gitlab_instance.auth()
+        return self._gitlab_instance
+
+    @property
+    def user(self) -> GitUser:
+        return GitlabUser(service=self)
+
+    def __str__(self) -> str:
+        token_str = (
+            f", token='{self.token[:1]}***{self.token[-1:]}'" if self.token else ""
+        )
+        ssl_str = ", ssl_verify=False" if not self.ssl_verify else ""
+        return (
+            f"GitlabService(instance_url='{self.instance_url}'"
+            f"{token_str}"
+            f"{ssl_str})"
+        )
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, GitlabService):
+            return False
+
+        return (
+            self.token == o.token  # type: ignore
+            and self.instance_url == o.instance_url  # type: ignore
+            and self.ssl_verify == o.ssl_verify  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(
+        self,
+        repo=None,
+        namespace=None,
+        is_fork=False,
+        **kwargs,
+    ) -> "GitlabProject":
+        if is_fork:
+            namespace = self.user.get_username()
+        return GitlabProject(repo=repo, namespace=namespace, service=self, **kwargs)
+
+    def get_project_from_project_id(self, iid: int) -> "GitlabProject":
+        gitlab_repo = self.gitlab_instance.projects.get(iid)
+        return GitlabProject(
+            repo=gitlab_repo.attributes["path"],
+            namespace=gitlab_repo.attributes["namespace"]["full_path"],
+            service=self,
+            gitlab_repo=gitlab_repo,
+        )
+
+    def change_token(self, new_token: str) -> None:
+        self.token = new_token
+        self._gitlab_instance = None
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "GitlabProject":
+        data = {"name": repo}
+        if namespace:
+            try:
+                group = self.gitlab_instance.groups.get(namespace)
+            except gitlab.GitlabGetError as ex:
+                raise GitlabAPIException(f"Group {namespace} not found.") from ex
+            data["namespace_id"] = group.id
+
+        if description:
+            data["description"] = description
+        try:
+            new_project = self.gitlab_instance.projects.create(data)
+        except gitlab.GitlabCreateError as ex:
+            raise GitlabAPIException("Project already exists") from ex
+        return GitlabProject(
+            repo=repo,
+            namespace=namespace,
+            service=self,
+            gitlab_repo=new_project,
+        )
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        if namespace:
+            group = self.gitlab_instance.groups.get(namespace)
+            projects = group.projects.list(all=True)
+        elif user:
+            user_object = self.gitlab_instance.users.list(username=user)[0]
+            projects = user_object.projects.list(all=True)
+        else:
+            raise OperationNotSupported
+
+        if language:
+            # group.projects.list gives us a GroupProject instance
+            # in order to be able to filter by language we need Project instance
+            projects_to_convert = [
+                self.gitlab_instance.projects.get(item.attributes["id"])
+                for item in projects
+                if language
+                in self.gitlab_instance.projects.get(item.attributes["id"]).languages()
+            ]
+        else:
+            projects_to_convert = projects
+        return [
+            GitlabProject(
+                repo=project.attributes["path"],
+                namespace=project.attributes["namespace"]["full_path"],
+                service=self,
+            )
+            for project in projects_to_convert
+        ]
+
+

Ancestors

+ +

Class variables

+
+
var name
+
+
+
+
+

Instance variables

+
+
var gitlab_instance : gitlab.client.Gitlab
+
+
+
+ +Expand source code + +
@property
+def gitlab_instance(self) -> gitlab.Gitlab:
+    if not self._gitlab_instance:
+        self._gitlab_instance = gitlab.Gitlab(
+            url=self.instance_url,
+            private_token=self.token,
+            ssl_verify=self.ssl_verify,
+        )
+        if self.token:
+            self._gitlab_instance.auth()
+    return self._gitlab_instance
+
+
+
+

Methods

+
+
+def get_project_from_project_id(self, iid: int) ‑> GitlabProject +
+
+
+
+ +Expand source code + +
def get_project_from_project_id(self, iid: int) -> "GitlabProject":
+    gitlab_repo = self.gitlab_instance.projects.get(iid)
+    return GitlabProject(
+        repo=gitlab_repo.attributes["path"],
+        namespace=gitlab_repo.attributes["namespace"]["full_path"],
+        service=self,
+        gitlab_repo=gitlab_repo,
+    )
+
+
+
+

Inherited members

+ +
+
+class GitlabUser +(service: ogr_gitlab.GitlabService) +
+
+

Represents currently authenticated user through service.

+
+ +Expand source code + +
class GitlabUser(BaseGitUser):
+    service: "ogr_gitlab.GitlabService"
+
+    def __init__(self, service: "ogr_gitlab.GitlabService") -> None:
+        super().__init__(service=service)
+
+    def __str__(self) -> str:
+        return f'GitlabUser(username="{self.get_username()}")'
+
+    @property
+    def _gitlab_user(self):
+        return self.service.gitlab_instance.user
+
+    def get_username(self) -> str:
+        return self._gitlab_user.username
+
+    def get_email(self) -> str:
+        return self._gitlab_user.email
+
+    def get_projects(self) -> list["ogr_gitlab.GitlabProject"]:
+        raise OperationNotSupported
+
+    def get_forks(self) -> list["ogr_gitlab.GitlabProject"]:
+        raise OperationNotSupported
+
+

Ancestors

+ +

Class variables

+
+
var serviceGitlabService
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/gitlab/issue.html b/docs/services/gitlab/issue.html new file mode 100644 index 00000000..be0fa0e7 --- /dev/null +++ b/docs/services/gitlab/issue.html @@ -0,0 +1,493 @@ + + + + + + +ogr.services.gitlab.issue API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.gitlab.issue

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from typing import Optional, Union
+
+import gitlab
+from gitlab.v4.objects import Issue as _GitlabIssue
+
+from ogr.abstract import Issue, IssueComment, IssueStatus
+from ogr.exceptions import GitlabAPIException, IssueTrackerDisabled
+from ogr.services import gitlab as ogr_gitlab
+from ogr.services.base import BaseIssue
+from ogr.services.gitlab.comments import GitlabIssueComment
+
+
+class GitlabIssue(BaseIssue):
+    _raw_issue: _GitlabIssue
+
+    @property
+    def title(self) -> str:
+        return self._raw_issue.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_issue.title = new_title
+        self._raw_issue.save()
+
+    @property
+    def id(self) -> int:
+        return self._raw_issue.iid
+
+    @property
+    def private(self) -> bool:
+        return self._raw_issue.confidential
+
+    @property
+    def status(self) -> IssueStatus:
+        return (
+            IssueStatus.open
+            if self._raw_issue.state == "opened"
+            else IssueStatus[self._raw_issue.state]
+        )
+
+    @property
+    def url(self) -> str:
+        return self._raw_issue.web_url
+
+    @property
+    def assignees(self) -> list:
+        try:
+            return self._raw_issue.assignees
+        except AttributeError:
+            return None  # if issue has no assignees, the attribute is not present
+
+    @property
+    def description(self) -> str:
+        return self._raw_issue.description
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_issue.description = new_description
+        self._raw_issue.save()
+
+    @property
+    def author(self) -> str:
+        return self._raw_issue.author["username"]
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_issue.created_at
+
+    @property
+    def labels(self) -> list:
+        return self._raw_issue.labels
+
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_gitlab.GitlabProject",
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        assignee_ids = []
+        for user in assignees or []:
+            users_list = project.service.gitlab_instance.users.list(username=user)
+
+            if not users_list:
+                raise GitlabAPIException(f"Unable to find '{user}' username")
+
+            assignee_ids.append(str(users_list[0].id))
+
+        data = {"title": title, "description": body}
+        if labels:
+            data["labels"] = ",".join(labels)
+        if assignees:
+            data["assignee_ids"] = ",".join(assignee_ids)
+
+        issue = project.gitlab_repo.issues.create(data, confidential=private)
+        return GitlabIssue(issue, project)
+
+    @staticmethod
+    def get(project: "ogr_gitlab.GitlabProject", issue_id: int) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        try:
+            return GitlabIssue(project.gitlab_repo.issues.get(issue_id), project)
+        except gitlab.exceptions.GitlabGetError as ex:
+            raise GitlabAPIException(f"Issue {issue_id} was not found. ") from ex
+
+    @staticmethod
+    def get_list(
+        project: "ogr_gitlab.GitlabProject",
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        # Gitlab API has status 'opened', not 'open'
+        parameters: dict[str, Union[str, list[str], bool]] = {
+            "state": status.name if status != IssueStatus.open else "opened",
+            "order_by": "updated_at",
+            "sort": "desc",
+            "all": True,
+        }
+        if author:
+            parameters["author_username"] = author
+        if assignee:
+            parameters["assignee_username"] = assignee
+        if labels:
+            parameters["labels"] = labels
+
+        issues = project.gitlab_repo.issues.list(**parameters)
+        return [GitlabIssue(issue, project) for issue in issues]
+
+    def _get_all_comments(self) -> list[IssueComment]:
+        return [
+            GitlabIssueComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_issue.notes.list(sort="asc", all=True)
+        ]
+
+    def comment(self, body: str) -> IssueComment:
+        comment = self._raw_issue.notes.create({"body": body})
+        return GitlabIssueComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "Issue":
+        self._raw_issue.state_event = "close"
+        self._raw_issue.save()
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        for label in labels:
+            self._raw_issue.labels.append(label)
+        self._raw_issue.save()
+
+    def add_assignee(self, *assignees: str) -> None:
+        assignee_ids = self._raw_issue.__dict__.get("assignee_ids") or []
+        for assignee in assignees:
+            users = self.project.service.gitlab_instance.users.list(  # type: ignore
+                username=assignee,
+            )
+            if not users:
+                raise GitlabAPIException(f"Unable to find '{assignee}' username")
+            uid = str(users[0].id)
+            if uid not in assignee_ids:
+                assignee_ids.append(str(users[0].id))
+
+        self._raw_issue.assignee_ids = assignee_ids
+        self._raw_issue.save()
+
+    def get_comment(self, comment_id: int) -> IssueComment:
+        return GitlabIssueComment(self._raw_issue.notes.get(comment_id))
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GitlabIssue +(raw_issue: Any, project: GitProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the issue.
+
+
+ +Expand source code + +
class GitlabIssue(BaseIssue):
+    _raw_issue: _GitlabIssue
+
+    @property
+    def title(self) -> str:
+        return self._raw_issue.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_issue.title = new_title
+        self._raw_issue.save()
+
+    @property
+    def id(self) -> int:
+        return self._raw_issue.iid
+
+    @property
+    def private(self) -> bool:
+        return self._raw_issue.confidential
+
+    @property
+    def status(self) -> IssueStatus:
+        return (
+            IssueStatus.open
+            if self._raw_issue.state == "opened"
+            else IssueStatus[self._raw_issue.state]
+        )
+
+    @property
+    def url(self) -> str:
+        return self._raw_issue.web_url
+
+    @property
+    def assignees(self) -> list:
+        try:
+            return self._raw_issue.assignees
+        except AttributeError:
+            return None  # if issue has no assignees, the attribute is not present
+
+    @property
+    def description(self) -> str:
+        return self._raw_issue.description
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_issue.description = new_description
+        self._raw_issue.save()
+
+    @property
+    def author(self) -> str:
+        return self._raw_issue.author["username"]
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_issue.created_at
+
+    @property
+    def labels(self) -> list:
+        return self._raw_issue.labels
+
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_gitlab.GitlabProject",
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        assignee_ids = []
+        for user in assignees or []:
+            users_list = project.service.gitlab_instance.users.list(username=user)
+
+            if not users_list:
+                raise GitlabAPIException(f"Unable to find '{user}' username")
+
+            assignee_ids.append(str(users_list[0].id))
+
+        data = {"title": title, "description": body}
+        if labels:
+            data["labels"] = ",".join(labels)
+        if assignees:
+            data["assignee_ids"] = ",".join(assignee_ids)
+
+        issue = project.gitlab_repo.issues.create(data, confidential=private)
+        return GitlabIssue(issue, project)
+
+    @staticmethod
+    def get(project: "ogr_gitlab.GitlabProject", issue_id: int) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        try:
+            return GitlabIssue(project.gitlab_repo.issues.get(issue_id), project)
+        except gitlab.exceptions.GitlabGetError as ex:
+            raise GitlabAPIException(f"Issue {issue_id} was not found. ") from ex
+
+    @staticmethod
+    def get_list(
+        project: "ogr_gitlab.GitlabProject",
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        # Gitlab API has status 'opened', not 'open'
+        parameters: dict[str, Union[str, list[str], bool]] = {
+            "state": status.name if status != IssueStatus.open else "opened",
+            "order_by": "updated_at",
+            "sort": "desc",
+            "all": True,
+        }
+        if author:
+            parameters["author_username"] = author
+        if assignee:
+            parameters["assignee_username"] = assignee
+        if labels:
+            parameters["labels"] = labels
+
+        issues = project.gitlab_repo.issues.list(**parameters)
+        return [GitlabIssue(issue, project) for issue in issues]
+
+    def _get_all_comments(self) -> list[IssueComment]:
+        return [
+            GitlabIssueComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_issue.notes.list(sort="asc", all=True)
+        ]
+
+    def comment(self, body: str) -> IssueComment:
+        comment = self._raw_issue.notes.create({"body": body})
+        return GitlabIssueComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "Issue":
+        self._raw_issue.state_event = "close"
+        self._raw_issue.save()
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        for label in labels:
+            self._raw_issue.labels.append(label)
+        self._raw_issue.save()
+
+    def add_assignee(self, *assignees: str) -> None:
+        assignee_ids = self._raw_issue.__dict__.get("assignee_ids") or []
+        for assignee in assignees:
+            users = self.project.service.gitlab_instance.users.list(  # type: ignore
+                username=assignee,
+            )
+            if not users:
+                raise GitlabAPIException(f"Unable to find '{assignee}' username")
+            uid = str(users[0].id)
+            if uid not in assignee_ids:
+                assignee_ids.append(str(users[0].id))
+
+        self._raw_issue.assignee_ids = assignee_ids
+        self._raw_issue.save()
+
+    def get_comment(self, comment_id: int) -> IssueComment:
+        return GitlabIssueComment(self._raw_issue.notes.get(comment_id))
+
+

Ancestors

+ +

Instance variables

+
+
var assignees : list
+
+
+
+ +Expand source code + +
@property
+def assignees(self) -> list:
+    try:
+        return self._raw_issue.assignees
+    except AttributeError:
+        return None  # if issue has no assignees, the attribute is not present
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/gitlab/project.html b/docs/services/gitlab/project.html new file mode 100644 index 00000000..565a863d --- /dev/null +++ b/docs/services/gitlab/project.html @@ -0,0 +1,1339 @@ + + + + + + +ogr.services.gitlab.project API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.gitlab.project

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import logging
+from typing import Any, Optional, Union
+
+import gitlab
+from gitlab.exceptions import GitlabGetError
+from gitlab.v4.objects import Project as GitlabObjectsProject
+from gitlab.v4.objects import ProjectCommit
+
+from ogr.abstract import (
+    AccessLevel,
+    CommitComment,
+    CommitFlag,
+    CommitStatus,
+    GitTag,
+    Issue,
+    IssueStatus,
+    PRStatus,
+    PullRequest,
+    Release,
+)
+from ogr.exceptions import GitlabAPIException, OperationNotSupported
+from ogr.services import gitlab as ogr_gitlab
+from ogr.services.base import BaseGitProject
+from ogr.services.gitlab.flag import GitlabCommitFlag
+from ogr.services.gitlab.issue import GitlabIssue
+from ogr.services.gitlab.pull_request import GitlabPullRequest
+from ogr.services.gitlab.release import GitlabRelease
+from ogr.utils import filter_paths, indirect
+
+logger = logging.getLogger(__name__)
+
+
+class GitlabProject(BaseGitProject):
+    service: "ogr_gitlab.GitlabService"
+
+    def __init__(
+        self,
+        repo: str,
+        service: "ogr_gitlab.GitlabService",
+        namespace: str,
+        gitlab_repo=None,
+        **unprocess_kwargs,
+    ) -> None:
+        if unprocess_kwargs:
+            logger.warning(
+                f"GitlabProject will not process these kwargs: {unprocess_kwargs}",
+            )
+        super().__init__(repo, service, namespace)
+        self._gitlab_repo = gitlab_repo
+        self.read_only = False
+
+    @property
+    def gitlab_repo(self) -> GitlabObjectsProject:
+        if not self._gitlab_repo:
+            self._gitlab_repo = self.service.gitlab_instance.projects.get(
+                f"{self.namespace}/{self.repo}",
+            )
+        return self._gitlab_repo
+
+    @property
+    def is_fork(self) -> bool:
+        return bool("forked_from_project" in self.gitlab_repo.attributes)
+
+    @property
+    def parent(self) -> Optional["GitlabProject"]:
+        if self.is_fork:
+            parent_dict = self.gitlab_repo.attributes["forked_from_project"]
+            return GitlabProject(
+                repo=parent_dict["path"],
+                service=self.service,
+                namespace=parent_dict["namespace"]["full_path"],
+            )
+        return None
+
+    @property
+    def default_branch(self) -> Optional[str]:
+        return self.gitlab_repo.attributes.get("default_branch")
+
+    def __str__(self) -> str:
+        return f'GitlabProject(namespace="{self.namespace}", repo="{self.repo}")'
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, GitlabProject):
+            return False
+
+        return (
+            self.repo == o.repo
+            and self.namespace == o.namespace
+            and self.service == o.service
+        )
+
+    @property
+    def has_issues(self) -> bool:
+        return self.gitlab_repo.issues_enabled
+
+    def _construct_fork_project(self) -> Optional["GitlabProject"]:
+        user_login = self.service.user.get_username()
+        try:
+            project = GitlabProject(
+                repo=self.repo,
+                service=self.service,
+                namespace=user_login,
+            )
+            if project.gitlab_repo:
+                return project
+        except Exception as ex:
+            logger.debug(f"Project {user_login}/{self.repo} does not exist: {ex}")
+        return None
+
+    def exists(self) -> bool:
+        try:
+            _ = self.gitlab_repo
+            return True
+        except gitlab.exceptions.GitlabGetError as ex:
+            if "404 Project Not Found" in str(ex):
+                return False
+            raise GitlabAPIException from ex
+
+    def is_private(self) -> bool:
+        return self.gitlab_repo.attributes["visibility"] == "private"
+
+    def is_forked(self) -> bool:
+        return bool(self._construct_fork_project())
+
+    def get_description(self) -> str:
+        return self.gitlab_repo.attributes["description"]
+
+    @property
+    def description(self) -> str:
+        return self.gitlab_repo.attributes["description"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.gitlab_repo.description = new_description
+        self.gitlab_repo.save()
+
+    def get_fork(self, create: bool = True) -> Optional["GitlabProject"]:
+        username = self.service.user.get_username()
+        for fork in self.get_forks():
+            if fork.gitlab_repo.namespace["full_path"] == username:
+                return fork
+
+        if not self.is_forked():
+            if create:
+                return self.fork_create()
+
+            logger.info(
+                f"Fork of {self.gitlab_repo.attributes['name']}"
+                " does not exist and we were asked not to create it.",
+            )
+            return None
+        return self._construct_fork_project()
+
+    def get_owners(self) -> list[str]:
+        return self._get_collaborators_with_given_access(
+            access_levels=[gitlab.const.OWNER_ACCESS],
+        )
+
+    def who_can_close_issue(self) -> set[str]:
+        return set(
+            self._get_collaborators_with_given_access(
+                access_levels=[
+                    gitlab.const.REPORTER_ACCESS,
+                    gitlab.const.DEVELOPER_ACCESS,
+                    gitlab.const.MAINTAINER_ACCESS,
+                    gitlab.const.OWNER_ACCESS,
+                ],
+            ),
+        )
+
+    def who_can_merge_pr(self) -> set[str]:
+        return set(
+            self._get_collaborators_with_given_access(
+                access_levels=[
+                    gitlab.const.DEVELOPER_ACCESS,
+                    gitlab.const.MAINTAINER_ACCESS,
+                    gitlab.const.OWNER_ACCESS,
+                ],
+            ),
+        )
+
+    def can_merge_pr(self, username) -> bool:
+        return username in self.who_can_merge_pr()
+
+    def delete(self) -> None:
+        self.gitlab_repo.delete()
+
+    def _get_collaborators_with_given_access(
+        self,
+        access_levels: list[int],
+    ) -> list[str]:
+        """
+        Get all project collaborators with one of the given access levels.
+        Access levels:
+            10 => Guest access
+            20 => Reporter access
+            30 => Developer access
+            40 => Maintainer access
+            50 => Owner access
+
+        Returns:
+            List of usernames.
+        """
+        # TODO: Remove once ‹members_all› is available for all releases of ogr
+        all_members = None
+        if hasattr(self.gitlab_repo, "members_all"):
+            all_members = self.gitlab_repo.members_all.list(all=True)
+        else:
+            all_members = self.gitlab_repo.members.all(all=True)
+
+        response = []
+        for member in all_members:
+            if isinstance(member, dict):
+                access_level = member["access_level"]
+                username = member["username"]
+            else:
+                access_level = member.access_level
+                username = member.username
+            if access_level in access_levels:
+                response.append(username)
+        return response
+
+    def add_user(self, user: str, access_level: AccessLevel) -> None:
+        access_dict = {
+            AccessLevel.pull: gitlab.const.GUEST_ACCESS,
+            AccessLevel.triage: gitlab.const.REPORTER_ACCESS,
+            AccessLevel.push: gitlab.const.DEVELOPER_ACCESS,
+            AccessLevel.admin: gitlab.const.MAINTAINER_ACCESS,
+            AccessLevel.maintain: gitlab.const.OWNER_ACCESS,
+        }
+        try:
+            user_id = self.service.gitlab_instance.users.list(username=user)[0].id
+        except Exception as e:
+            raise GitlabAPIException(f"User {user} not found") from e
+        try:
+            self.gitlab_repo.members.create(
+                {"user_id": user_id, "access_level": access_dict[access_level]},
+            )
+        except Exception as e:
+            raise GitlabAPIException(f"User {user} already exists") from e
+
+    def request_access(self) -> None:
+        try:
+            self.gitlab_repo.accessrequests.create({})
+        except gitlab.exceptions.GitlabCreateError as e:
+            raise GitlabAPIException("Unable to request access") from e
+
+    @indirect(GitlabPullRequest.get_list)
+    def get_pr_list(self, status: PRStatus = PRStatus.open) -> list["PullRequest"]:
+        pass
+
+    def get_sha_from_tag(self, tag_name: str) -> str:
+        try:
+            tag = self.gitlab_repo.tags.get(tag_name)
+            return tag.attributes["commit"]["id"]
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Tag {tag_name} was not found.")
+            raise GitlabAPIException(f"Tag {tag_name} was not found.") from ex
+
+    @indirect(GitlabPullRequest.create)
+    def create_pr(
+        self,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        pass
+
+    def commit_comment(
+        self,
+        commit: str,
+        body: str,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "CommitComment":
+        try:
+            commit_object: ProjectCommit = self.gitlab_repo.commits.get(commit)
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Commit {commit} was not found.")
+            raise GitlabAPIException(f"Commit {commit} was not found.") from ex
+
+        if filename and row:
+            raw_comment = commit_object.comments.create(
+                {"note": body, "path": filename, "line": row, "line_type": "new"},
+            )
+        else:
+            raw_comment = commit_object.comments.create({"note": body})
+        return self._commit_comment_from_gitlab_object(raw_comment, commit)
+
+    @staticmethod
+    def _commit_comment_from_gitlab_object(raw_comment, commit) -> CommitComment:
+        return CommitComment(
+            sha=commit,
+            body=raw_comment.note,
+            author=raw_comment.author["username"],
+        )
+
+    def get_commit_comments(self, commit: str) -> list[CommitComment]:
+        try:
+            commit_object: ProjectCommit = self.gitlab_repo.commits.get(commit)
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Commit {commit} was not found.")
+            raise GitlabAPIException(f"Commit {commit} was not found.") from ex
+
+        return [
+            self._commit_comment_from_gitlab_object(comment, commit)
+            for comment in commit_object.comments.list()
+        ]
+
+    @indirect(GitlabCommitFlag.set)
+    def set_commit_status(
+        self,
+        commit: str,
+        state: Union[CommitStatus, str],
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        pass
+
+    @indirect(GitlabCommitFlag.get)
+    def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+        pass
+
+    def get_git_urls(self) -> dict[str, str]:
+        return {
+            "git": self.gitlab_repo.attributes["http_url_to_repo"],
+            "ssh": self.gitlab_repo.attributes["ssh_url_to_repo"],
+        }
+
+    def fork_create(self, namespace: Optional[str] = None) -> "GitlabProject":
+        data = {}
+        if namespace:
+            data["namespace_path"] = namespace
+
+        try:
+            fork = self.gitlab_repo.forks.create(data=data)
+        except gitlab.GitlabCreateError as ex:
+            logger.error(f"Repo {self.gitlab_repo} cannot be forked")
+            raise GitlabAPIException(
+                f"Repo {self.gitlab_repo} cannot be forked",
+            ) from ex
+        logger.debug(f"Forked to {fork.namespace['full_path']}/{fork.path}")
+        return GitlabProject(
+            namespace=fork.namespace["full_path"],
+            service=self.service,
+            repo=fork.path,
+        )
+
+    def change_token(self, new_token: str):
+        self.service.change_token(new_token)
+
+    def get_branches(self) -> list[str]:
+        return [branch.name for branch in self.gitlab_repo.branches.list(all=True)]
+
+    def get_file_content(self, path, ref=None) -> str:
+        ref = ref or self.default_branch
+        try:
+            file = self.gitlab_repo.files.get(file_path=path, ref=ref)
+            return file.decode().decode()
+        except gitlab.exceptions.GitlabGetError as ex:
+            if ex.response_code == 404:
+                raise FileNotFoundError(f"File '{path}' on {ref} not found") from ex
+            raise GitlabAPIException() from ex
+
+    def get_files(
+        self,
+        ref: Optional[str] = None,
+        filter_regex: Optional[str] = None,
+        recursive: bool = False,
+    ) -> list[str]:
+        ref = ref or self.default_branch
+        paths = [
+            file_dict["path"]
+            for file_dict in self.gitlab_repo.repository_tree(
+                ref=ref,
+                recursive=recursive,
+                all=True,
+            )
+            if file_dict["type"] != "tree"
+        ]
+        if filter_regex:
+            paths = filter_paths(paths, filter_regex)
+
+        return paths
+
+    @indirect(GitlabIssue.get_list)
+    def get_issue_list(
+        self,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list[Issue]:
+        pass
+
+    @indirect(GitlabIssue.get)
+    def get_issue(self, issue_id: int) -> Issue:
+        pass
+
+    @indirect(GitlabIssue.create)
+    def create_issue(
+        self,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> Issue:
+        pass
+
+    @indirect(GitlabPullRequest.get)
+    def get_pr(self, pr_id: int) -> PullRequest:
+        pass
+
+    def get_tags(self) -> list["GitTag"]:
+        tags = self.gitlab_repo.tags.list()
+        return [GitTag(tag.name, tag.commit["id"]) for tag in tags]
+
+    def _git_tag_from_tag_name(self, tag_name: str) -> GitTag:
+        git_tag = self.gitlab_repo.tags.get(tag_name)
+        return GitTag(name=git_tag.name, commit_sha=git_tag.commit["id"])
+
+    @indirect(GitlabRelease.get_list)
+    def get_releases(self) -> list[Release]:
+        pass
+
+    @indirect(GitlabRelease.get)
+    def get_release(self, identifier=None, name=None, tag_name=None) -> GitlabRelease:
+        pass
+
+    @indirect(GitlabRelease.create)
+    def create_release(
+        self,
+        tag: str,
+        name: str,
+        message: str,
+        commit_sha: Optional[str] = None,
+    ) -> GitlabRelease:
+        pass
+
+    @indirect(GitlabRelease.get_latest)
+    def get_latest_release(self) -> Optional[GitlabRelease]:
+        pass
+
+    def list_labels(self):
+        """
+        Get list of labels in the repository.
+
+        Returns:
+            List of labels in the repository.
+        """
+        return list(self.gitlab_repo.labels.list())
+
+    def get_forks(self) -> list["GitlabProject"]:
+        try:
+            forks = self.gitlab_repo.forks.list()
+        except KeyError as ex:
+            # > item = self._data[self._current]
+            # > KeyError: 0
+            # looks like some API weirdness
+            raise OperationNotSupported(
+                "Please upgrade python-gitlab to a newer version.",
+            ) from ex
+        return [
+            GitlabProject(
+                repo=fork.path,
+                namespace=fork.namespace["full_path"],
+                service=self.service,
+            )
+            for fork in forks
+        ]
+
+    def update_labels(self, labels):
+        """
+        Update the labels of the repository. (No deletion, only add not existing ones.)
+
+        Args:
+            labels: List of labels to be added.
+
+        Returns:
+            Number of added labels.
+        """
+        current_label_names = [la.name for la in list(self.gitlab_repo.labels.list())]
+        changes = 0
+        for label in labels:
+            if label.name not in current_label_names:
+                color = self._normalize_label_color(color=label.color)
+                self.gitlab_repo.labels.create(
+                    {
+                        "name": label.name,
+                        "color": color,
+                        "description": label.description or "",
+                    },
+                )
+
+                changes += 1
+        return changes
+
+    @staticmethod
+    def _normalize_label_color(color):
+        if not color.startswith("#"):
+            return f"#{color}"
+        return color
+
+    def get_web_url(self) -> str:
+        return self.gitlab_repo.web_url
+
+    def get_sha_from_branch(self, branch: str) -> Optional[str]:
+        try:
+            return self.gitlab_repo.branches.get(branch).attributes["commit"]["id"]
+        except GitlabGetError as ex:
+            if ex.response_code == 404:
+                return None
+            raise GitlabAPIException from ex
+
+    def get_contributors(self) -> set[str]:
+        """
+        Returns:
+            Unique authors of the commits in the project.
+        """
+
+        def format_contributor(contributor: dict[str, Any]) -> str:
+            return f"{contributor['name']} <{contributor['email']}>"
+
+        return set(
+            map(format_contributor, self.gitlab_repo.repository_contributors(all=True)),
+        )
+
+    def users_with_write_access(self) -> set[str]:
+        return set(
+            self._get_collaborators_with_given_access(
+                access_levels=[
+                    gitlab.const.DEVELOPER_ACCESS,
+                    gitlab.const.MAINTAINER_ACCESS,
+                    gitlab.const.OWNER_ACCESS,
+                ],
+            ),
+        )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GitlabProject +(repo: str, service: ogr_gitlab.GitlabService, namespace: str, gitlab_repo=None, **unprocess_kwargs) +
+
+

Args

+
+
repo
+
Name of the project.
+
service
+
GitService instance.
+
namespace
+
+

Namespace of the project.

+
    +
  • GitHub: username or org name.
  • +
  • GitLab: username or org name.
  • +
  • Pagure: namespace (e.g. "rpms").
  • +
+

In case of forks: "fork/{username}/{namespace}".

+
+
+
+ +Expand source code + +
class GitlabProject(BaseGitProject):
+    service: "ogr_gitlab.GitlabService"
+
+    def __init__(
+        self,
+        repo: str,
+        service: "ogr_gitlab.GitlabService",
+        namespace: str,
+        gitlab_repo=None,
+        **unprocess_kwargs,
+    ) -> None:
+        if unprocess_kwargs:
+            logger.warning(
+                f"GitlabProject will not process these kwargs: {unprocess_kwargs}",
+            )
+        super().__init__(repo, service, namespace)
+        self._gitlab_repo = gitlab_repo
+        self.read_only = False
+
+    @property
+    def gitlab_repo(self) -> GitlabObjectsProject:
+        if not self._gitlab_repo:
+            self._gitlab_repo = self.service.gitlab_instance.projects.get(
+                f"{self.namespace}/{self.repo}",
+            )
+        return self._gitlab_repo
+
+    @property
+    def is_fork(self) -> bool:
+        return bool("forked_from_project" in self.gitlab_repo.attributes)
+
+    @property
+    def parent(self) -> Optional["GitlabProject"]:
+        if self.is_fork:
+            parent_dict = self.gitlab_repo.attributes["forked_from_project"]
+            return GitlabProject(
+                repo=parent_dict["path"],
+                service=self.service,
+                namespace=parent_dict["namespace"]["full_path"],
+            )
+        return None
+
+    @property
+    def default_branch(self) -> Optional[str]:
+        return self.gitlab_repo.attributes.get("default_branch")
+
+    def __str__(self) -> str:
+        return f'GitlabProject(namespace="{self.namespace}", repo="{self.repo}")'
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, GitlabProject):
+            return False
+
+        return (
+            self.repo == o.repo
+            and self.namespace == o.namespace
+            and self.service == o.service
+        )
+
+    @property
+    def has_issues(self) -> bool:
+        return self.gitlab_repo.issues_enabled
+
+    def _construct_fork_project(self) -> Optional["GitlabProject"]:
+        user_login = self.service.user.get_username()
+        try:
+            project = GitlabProject(
+                repo=self.repo,
+                service=self.service,
+                namespace=user_login,
+            )
+            if project.gitlab_repo:
+                return project
+        except Exception as ex:
+            logger.debug(f"Project {user_login}/{self.repo} does not exist: {ex}")
+        return None
+
+    def exists(self) -> bool:
+        try:
+            _ = self.gitlab_repo
+            return True
+        except gitlab.exceptions.GitlabGetError as ex:
+            if "404 Project Not Found" in str(ex):
+                return False
+            raise GitlabAPIException from ex
+
+    def is_private(self) -> bool:
+        return self.gitlab_repo.attributes["visibility"] == "private"
+
+    def is_forked(self) -> bool:
+        return bool(self._construct_fork_project())
+
+    def get_description(self) -> str:
+        return self.gitlab_repo.attributes["description"]
+
+    @property
+    def description(self) -> str:
+        return self.gitlab_repo.attributes["description"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.gitlab_repo.description = new_description
+        self.gitlab_repo.save()
+
+    def get_fork(self, create: bool = True) -> Optional["GitlabProject"]:
+        username = self.service.user.get_username()
+        for fork in self.get_forks():
+            if fork.gitlab_repo.namespace["full_path"] == username:
+                return fork
+
+        if not self.is_forked():
+            if create:
+                return self.fork_create()
+
+            logger.info(
+                f"Fork of {self.gitlab_repo.attributes['name']}"
+                " does not exist and we were asked not to create it.",
+            )
+            return None
+        return self._construct_fork_project()
+
+    def get_owners(self) -> list[str]:
+        return self._get_collaborators_with_given_access(
+            access_levels=[gitlab.const.OWNER_ACCESS],
+        )
+
+    def who_can_close_issue(self) -> set[str]:
+        return set(
+            self._get_collaborators_with_given_access(
+                access_levels=[
+                    gitlab.const.REPORTER_ACCESS,
+                    gitlab.const.DEVELOPER_ACCESS,
+                    gitlab.const.MAINTAINER_ACCESS,
+                    gitlab.const.OWNER_ACCESS,
+                ],
+            ),
+        )
+
+    def who_can_merge_pr(self) -> set[str]:
+        return set(
+            self._get_collaborators_with_given_access(
+                access_levels=[
+                    gitlab.const.DEVELOPER_ACCESS,
+                    gitlab.const.MAINTAINER_ACCESS,
+                    gitlab.const.OWNER_ACCESS,
+                ],
+            ),
+        )
+
+    def can_merge_pr(self, username) -> bool:
+        return username in self.who_can_merge_pr()
+
+    def delete(self) -> None:
+        self.gitlab_repo.delete()
+
+    def _get_collaborators_with_given_access(
+        self,
+        access_levels: list[int],
+    ) -> list[str]:
+        """
+        Get all project collaborators with one of the given access levels.
+        Access levels:
+            10 => Guest access
+            20 => Reporter access
+            30 => Developer access
+            40 => Maintainer access
+            50 => Owner access
+
+        Returns:
+            List of usernames.
+        """
+        # TODO: Remove once ‹members_all› is available for all releases of ogr
+        all_members = None
+        if hasattr(self.gitlab_repo, "members_all"):
+            all_members = self.gitlab_repo.members_all.list(all=True)
+        else:
+            all_members = self.gitlab_repo.members.all(all=True)
+
+        response = []
+        for member in all_members:
+            if isinstance(member, dict):
+                access_level = member["access_level"]
+                username = member["username"]
+            else:
+                access_level = member.access_level
+                username = member.username
+            if access_level in access_levels:
+                response.append(username)
+        return response
+
+    def add_user(self, user: str, access_level: AccessLevel) -> None:
+        access_dict = {
+            AccessLevel.pull: gitlab.const.GUEST_ACCESS,
+            AccessLevel.triage: gitlab.const.REPORTER_ACCESS,
+            AccessLevel.push: gitlab.const.DEVELOPER_ACCESS,
+            AccessLevel.admin: gitlab.const.MAINTAINER_ACCESS,
+            AccessLevel.maintain: gitlab.const.OWNER_ACCESS,
+        }
+        try:
+            user_id = self.service.gitlab_instance.users.list(username=user)[0].id
+        except Exception as e:
+            raise GitlabAPIException(f"User {user} not found") from e
+        try:
+            self.gitlab_repo.members.create(
+                {"user_id": user_id, "access_level": access_dict[access_level]},
+            )
+        except Exception as e:
+            raise GitlabAPIException(f"User {user} already exists") from e
+
+    def request_access(self) -> None:
+        try:
+            self.gitlab_repo.accessrequests.create({})
+        except gitlab.exceptions.GitlabCreateError as e:
+            raise GitlabAPIException("Unable to request access") from e
+
+    @indirect(GitlabPullRequest.get_list)
+    def get_pr_list(self, status: PRStatus = PRStatus.open) -> list["PullRequest"]:
+        pass
+
+    def get_sha_from_tag(self, tag_name: str) -> str:
+        try:
+            tag = self.gitlab_repo.tags.get(tag_name)
+            return tag.attributes["commit"]["id"]
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Tag {tag_name} was not found.")
+            raise GitlabAPIException(f"Tag {tag_name} was not found.") from ex
+
+    @indirect(GitlabPullRequest.create)
+    def create_pr(
+        self,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        pass
+
+    def commit_comment(
+        self,
+        commit: str,
+        body: str,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "CommitComment":
+        try:
+            commit_object: ProjectCommit = self.gitlab_repo.commits.get(commit)
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Commit {commit} was not found.")
+            raise GitlabAPIException(f"Commit {commit} was not found.") from ex
+
+        if filename and row:
+            raw_comment = commit_object.comments.create(
+                {"note": body, "path": filename, "line": row, "line_type": "new"},
+            )
+        else:
+            raw_comment = commit_object.comments.create({"note": body})
+        return self._commit_comment_from_gitlab_object(raw_comment, commit)
+
+    @staticmethod
+    def _commit_comment_from_gitlab_object(raw_comment, commit) -> CommitComment:
+        return CommitComment(
+            sha=commit,
+            body=raw_comment.note,
+            author=raw_comment.author["username"],
+        )
+
+    def get_commit_comments(self, commit: str) -> list[CommitComment]:
+        try:
+            commit_object: ProjectCommit = self.gitlab_repo.commits.get(commit)
+        except gitlab.exceptions.GitlabGetError as ex:
+            logger.error(f"Commit {commit} was not found.")
+            raise GitlabAPIException(f"Commit {commit} was not found.") from ex
+
+        return [
+            self._commit_comment_from_gitlab_object(comment, commit)
+            for comment in commit_object.comments.list()
+        ]
+
+    @indirect(GitlabCommitFlag.set)
+    def set_commit_status(
+        self,
+        commit: str,
+        state: Union[CommitStatus, str],
+        target_url: str,
+        description: str,
+        context: str,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        pass
+
+    @indirect(GitlabCommitFlag.get)
+    def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+        pass
+
+    def get_git_urls(self) -> dict[str, str]:
+        return {
+            "git": self.gitlab_repo.attributes["http_url_to_repo"],
+            "ssh": self.gitlab_repo.attributes["ssh_url_to_repo"],
+        }
+
+    def fork_create(self, namespace: Optional[str] = None) -> "GitlabProject":
+        data = {}
+        if namespace:
+            data["namespace_path"] = namespace
+
+        try:
+            fork = self.gitlab_repo.forks.create(data=data)
+        except gitlab.GitlabCreateError as ex:
+            logger.error(f"Repo {self.gitlab_repo} cannot be forked")
+            raise GitlabAPIException(
+                f"Repo {self.gitlab_repo} cannot be forked",
+            ) from ex
+        logger.debug(f"Forked to {fork.namespace['full_path']}/{fork.path}")
+        return GitlabProject(
+            namespace=fork.namespace["full_path"],
+            service=self.service,
+            repo=fork.path,
+        )
+
+    def change_token(self, new_token: str):
+        self.service.change_token(new_token)
+
+    def get_branches(self) -> list[str]:
+        return [branch.name for branch in self.gitlab_repo.branches.list(all=True)]
+
+    def get_file_content(self, path, ref=None) -> str:
+        ref = ref or self.default_branch
+        try:
+            file = self.gitlab_repo.files.get(file_path=path, ref=ref)
+            return file.decode().decode()
+        except gitlab.exceptions.GitlabGetError as ex:
+            if ex.response_code == 404:
+                raise FileNotFoundError(f"File '{path}' on {ref} not found") from ex
+            raise GitlabAPIException() from ex
+
+    def get_files(
+        self,
+        ref: Optional[str] = None,
+        filter_regex: Optional[str] = None,
+        recursive: bool = False,
+    ) -> list[str]:
+        ref = ref or self.default_branch
+        paths = [
+            file_dict["path"]
+            for file_dict in self.gitlab_repo.repository_tree(
+                ref=ref,
+                recursive=recursive,
+                all=True,
+            )
+            if file_dict["type"] != "tree"
+        ]
+        if filter_regex:
+            paths = filter_paths(paths, filter_regex)
+
+        return paths
+
+    @indirect(GitlabIssue.get_list)
+    def get_issue_list(
+        self,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list[Issue]:
+        pass
+
+    @indirect(GitlabIssue.get)
+    def get_issue(self, issue_id: int) -> Issue:
+        pass
+
+    @indirect(GitlabIssue.create)
+    def create_issue(
+        self,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> Issue:
+        pass
+
+    @indirect(GitlabPullRequest.get)
+    def get_pr(self, pr_id: int) -> PullRequest:
+        pass
+
+    def get_tags(self) -> list["GitTag"]:
+        tags = self.gitlab_repo.tags.list()
+        return [GitTag(tag.name, tag.commit["id"]) for tag in tags]
+
+    def _git_tag_from_tag_name(self, tag_name: str) -> GitTag:
+        git_tag = self.gitlab_repo.tags.get(tag_name)
+        return GitTag(name=git_tag.name, commit_sha=git_tag.commit["id"])
+
+    @indirect(GitlabRelease.get_list)
+    def get_releases(self) -> list[Release]:
+        pass
+
+    @indirect(GitlabRelease.get)
+    def get_release(self, identifier=None, name=None, tag_name=None) -> GitlabRelease:
+        pass
+
+    @indirect(GitlabRelease.create)
+    def create_release(
+        self,
+        tag: str,
+        name: str,
+        message: str,
+        commit_sha: Optional[str] = None,
+    ) -> GitlabRelease:
+        pass
+
+    @indirect(GitlabRelease.get_latest)
+    def get_latest_release(self) -> Optional[GitlabRelease]:
+        pass
+
+    def list_labels(self):
+        """
+        Get list of labels in the repository.
+
+        Returns:
+            List of labels in the repository.
+        """
+        return list(self.gitlab_repo.labels.list())
+
+    def get_forks(self) -> list["GitlabProject"]:
+        try:
+            forks = self.gitlab_repo.forks.list()
+        except KeyError as ex:
+            # > item = self._data[self._current]
+            # > KeyError: 0
+            # looks like some API weirdness
+            raise OperationNotSupported(
+                "Please upgrade python-gitlab to a newer version.",
+            ) from ex
+        return [
+            GitlabProject(
+                repo=fork.path,
+                namespace=fork.namespace["full_path"],
+                service=self.service,
+            )
+            for fork in forks
+        ]
+
+    def update_labels(self, labels):
+        """
+        Update the labels of the repository. (No deletion, only add not existing ones.)
+
+        Args:
+            labels: List of labels to be added.
+
+        Returns:
+            Number of added labels.
+        """
+        current_label_names = [la.name for la in list(self.gitlab_repo.labels.list())]
+        changes = 0
+        for label in labels:
+            if label.name not in current_label_names:
+                color = self._normalize_label_color(color=label.color)
+                self.gitlab_repo.labels.create(
+                    {
+                        "name": label.name,
+                        "color": color,
+                        "description": label.description or "",
+                    },
+                )
+
+                changes += 1
+        return changes
+
+    @staticmethod
+    def _normalize_label_color(color):
+        if not color.startswith("#"):
+            return f"#{color}"
+        return color
+
+    def get_web_url(self) -> str:
+        return self.gitlab_repo.web_url
+
+    def get_sha_from_branch(self, branch: str) -> Optional[str]:
+        try:
+            return self.gitlab_repo.branches.get(branch).attributes["commit"]["id"]
+        except GitlabGetError as ex:
+            if ex.response_code == 404:
+                return None
+            raise GitlabAPIException from ex
+
+    def get_contributors(self) -> set[str]:
+        """
+        Returns:
+            Unique authors of the commits in the project.
+        """
+
+        def format_contributor(contributor: dict[str, Any]) -> str:
+            return f"{contributor['name']} <{contributor['email']}>"
+
+        return set(
+            map(format_contributor, self.gitlab_repo.repository_contributors(all=True)),
+        )
+
+    def users_with_write_access(self) -> set[str]:
+        return set(
+            self._get_collaborators_with_given_access(
+                access_levels=[
+                    gitlab.const.DEVELOPER_ACCESS,
+                    gitlab.const.MAINTAINER_ACCESS,
+                    gitlab.const.OWNER_ACCESS,
+                ],
+            ),
+        )
+
+

Ancestors

+ +

Class variables

+
+
var serviceGitlabService
+
+
+
+
+

Instance variables

+
+
var gitlab_repo : gitlab.v4.objects.projects.Project
+
+
+
+ +Expand source code + +
@property
+def gitlab_repo(self) -> GitlabObjectsProject:
+    if not self._gitlab_repo:
+        self._gitlab_repo = self.service.gitlab_instance.projects.get(
+            f"{self.namespace}/{self.repo}",
+        )
+    return self._gitlab_repo
+
+
+
+

Methods

+
+
+def get_contributors(self) ‑> set[str] +
+
+

Returns

+

Unique authors of the commits in the project.

+
+ +Expand source code + +
def get_contributors(self) -> set[str]:
+    """
+    Returns:
+        Unique authors of the commits in the project.
+    """
+
+    def format_contributor(contributor: dict[str, Any]) -> str:
+        return f"{contributor['name']} <{contributor['email']}>"
+
+    return set(
+        map(format_contributor, self.gitlab_repo.repository_contributors(all=True)),
+    )
+
+
+
+def list_labels(self) +
+
+

Get list of labels in the repository.

+

Returns

+

List of labels in the repository.

+
+ +Expand source code + +
def list_labels(self):
+    """
+    Get list of labels in the repository.
+
+    Returns:
+        List of labels in the repository.
+    """
+    return list(self.gitlab_repo.labels.list())
+
+
+
+def update_labels(self, labels) +
+
+

Update the labels of the repository. (No deletion, only add not existing ones.)

+

Args

+
+
labels
+
List of labels to be added.
+
+

Returns

+

Number of added labels.

+
+ +Expand source code + +
def update_labels(self, labels):
+    """
+    Update the labels of the repository. (No deletion, only add not existing ones.)
+
+    Args:
+        labels: List of labels to be added.
+
+    Returns:
+        Number of added labels.
+    """
+    current_label_names = [la.name for la in list(self.gitlab_repo.labels.list())]
+    changes = 0
+    for label in labels:
+        if label.name not in current_label_names:
+            color = self._normalize_label_color(color=label.color)
+            self.gitlab_repo.labels.create(
+                {
+                    "name": label.name,
+                    "color": color,
+                    "description": label.description or "",
+                },
+            )
+
+            changes += 1
+    return changes
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/gitlab/pull_request.html b/docs/services/gitlab/pull_request.html new file mode 100644 index 00000000..e54b4cb3 --- /dev/null +++ b/docs/services/gitlab/pull_request.html @@ -0,0 +1,794 @@ + + + + + + +ogr.services.gitlab.pull_request API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.gitlab.pull_request

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from typing import ClassVar, Optional
+
+import gitlab
+import requests
+from gitlab.exceptions import GitlabGetError
+from gitlab.v4.objects import MergeRequest as _GitlabMergeRequest
+
+from ogr.abstract import MergeCommitStatus, PRComment, PRStatus, PullRequest
+from ogr.exceptions import GitlabAPIException, OgrNetworkError
+from ogr.services import gitlab as ogr_gitlab
+from ogr.services.base import BasePullRequest
+from ogr.services.gitlab.comments import GitlabPRComment
+
+
+class GitlabPullRequest(BasePullRequest):
+    _raw_pr: _GitlabMergeRequest
+    _target_project: "ogr_gitlab.GitlabProject"
+    _source_project: "ogr_gitlab.GitlabProject" = None
+    _merge_commit_status: ClassVar[dict[str, MergeCommitStatus]] = {
+        "can_be_merged": MergeCommitStatus.can_be_merged,
+        "cannot_be_merged": MergeCommitStatus.cannot_be_merged,
+        "unchecked": MergeCommitStatus.unchecked,
+        "checking": MergeCommitStatus.checking,
+        "cannot_be_merged_recheck": MergeCommitStatus.cannot_be_merged_recheck,
+    }
+
+    @property
+    def title(self) -> str:
+        return self._raw_pr.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_pr.title = new_title
+        self._raw_pr.save()
+
+    @property
+    def id(self) -> int:
+        return self._raw_pr.iid
+
+    @property
+    def status(self) -> PRStatus:
+        return (
+            PRStatus.open
+            if self._raw_pr.state == "opened"
+            else PRStatus[self._raw_pr.state]
+        )
+
+    @property
+    def url(self) -> str:
+        return self._raw_pr.web_url
+
+    @property
+    def description(self) -> str:
+        return self._raw_pr.description
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_pr.description = new_description
+        self._raw_pr.save()
+
+    @property
+    def author(self) -> str:
+        return self._raw_pr.author["username"]
+
+    @property
+    def source_branch(self) -> str:
+        return self._raw_pr.source_branch
+
+    @property
+    def target_branch(self) -> str:
+        return self._raw_pr.target_branch
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_pr.created_at
+
+    @property
+    def labels(self) -> list[str]:
+        return self._raw_pr.labels
+
+    @property
+    def diff_url(self) -> str:
+        return f"{self._raw_pr.web_url}/diffs"
+
+    @property
+    def commits_url(self) -> str:
+        return f"{self._raw_pr.web_url}/commits"
+
+    @property
+    def patch(self) -> bytes:
+        response = requests.get(f"{self.url}.patch")
+
+        if not response.ok:
+            cls = OgrNetworkError if response.status_code >= 500 else GitlabAPIException
+            raise cls(
+                f"Couldn't get patch from {self.url}.patch because {response.reason}.",
+            )
+
+        return response.content
+
+    @property
+    def head_commit(self) -> str:
+        return self._raw_pr.sha
+
+    @property
+    def merge_commit_sha(self) -> Optional[str]:
+        # when merged => return merge_commit_sha
+        # otherwise => return test merge if possible
+        if self.status == PRStatus.merged:
+            return self._raw_pr.merge_commit_sha
+
+        # works for test merge only with python-gitlab>=2.10.0
+        try:
+            response = self._raw_pr.merge_ref()
+        except GitlabGetError as ex:
+            if ex.response_code == 400:
+                return None
+            raise
+        return response.get("commit_id")
+
+    @property
+    def merge_commit_status(self) -> MergeCommitStatus:
+        status = self._raw_pr.merge_status
+        if status not in self._merge_commit_status:
+            raise GitlabAPIException(f"Invalid merge_status {status}")
+
+        return self._merge_commit_status[status]
+
+    @property
+    def source_project(self) -> "ogr_gitlab.GitlabProject":
+        if self._source_project is None:
+            self._source_project = (
+                self._target_project.service.get_project_from_project_id(
+                    self._raw_pr.attributes["source_project_id"],
+                )
+            )
+        return self._source_project
+
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_gitlab.GitlabProject",
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        How to create PR:
+        -  upstream -> upstream - call on upstream, fork_username unset
+        -  fork -> upstream - call on fork, fork_username unset
+           also can call on upstream with fork_username, not supported way of using
+        -  fork -> fork - call on fork, fork_username set
+        -  fork -> other_fork - call on fork, fork_username set to other_fork owner
+        """
+        repo = project.gitlab_repo
+        parameters = {
+            "source_branch": source_branch,
+            "target_branch": target_branch,
+            "title": title,
+            "description": body,
+        }
+        target_id = None
+
+        target_project = project
+        if project.is_fork and fork_username is None:
+            # handles fork -> upstream (called on fork)
+            target_id = project.parent.gitlab_repo.attributes["id"]
+            target_project = project.parent
+        elif fork_username and fork_username != project.namespace:
+            # handles fork -> upstream
+            #   (username of fork owner specified by fork_username)
+            # handles fork -> other_fork
+            #   (username of other_fork owner specified by fork_username)
+
+            other_project = GitlabPullRequest.__get_fork(
+                fork_username,
+                project if project.parent is None else project.parent,
+            )
+
+            target_id = other_project.gitlab_repo.attributes["id"]
+            if project.parent is None:
+                target_id = repo.attributes["id"]
+                repo = other_project.gitlab_repo
+        # otherwise handles PR from the same project to same project
+
+        if target_id is not None:
+            parameters["target_project_id"] = target_id
+
+        mr = repo.mergerequests.create(parameters)
+        return GitlabPullRequest(mr, target_project)
+
+    @staticmethod
+    def __get_fork(
+        fork_username: str,
+        project: "ogr_gitlab.GitlabProject",
+    ) -> "ogr_gitlab.GitlabProject":
+        """
+        Returns forked project of a requested user. Internal method, in case the fork
+        doesn't exist, raises GitlabAPIException.
+
+        Args:
+            fork_username: Username of a user that owns requested fork.
+            project: Project to search forks of.
+
+        Returns:
+            Requested fork.
+
+        Raises:
+            GitlabAPIException, in case the fork doesn't exist.
+        """
+        forks = list(
+            filter(
+                lambda fork: fork.gitlab_repo.namespace["full_path"] == fork_username,
+                project.get_forks(),
+            ),
+        )
+        if not forks:
+            raise GitlabAPIException("Requested fork doesn't exist")
+        return forks[0]
+
+    @staticmethod
+    def get(project: "ogr_gitlab.GitlabProject", pr_id: int) -> "PullRequest":
+        try:
+            mr = project.gitlab_repo.mergerequests.get(pr_id)
+        except gitlab.GitlabGetError as ex:
+            raise GitlabAPIException(f"No PR with id {pr_id} found") from ex
+        return GitlabPullRequest(mr, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_gitlab.GitlabProject",
+        status: PRStatus = PRStatus.open,
+    ) -> list["PullRequest"]:
+        # Gitlab API has status 'opened', not 'open'
+        # f"Calling a `list()` method without specifying `get_all=True` or "
+        # f"`iterator=True` will return a maximum of 20 items. "
+        mrs = project.gitlab_repo.mergerequests.list(
+            state=status.name if status != PRStatus.open else "opened",
+            order_by="updated_at",
+            sort="desc",
+            # gitlab 3.3 syntax was all=True
+            get_all=True,
+        )
+        return [GitlabPullRequest(mr, project) for mr in mrs]
+
+    def update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "PullRequest":
+        if title:
+            self._raw_pr.title = title
+        if description:
+            self._raw_pr.description = description
+
+        self._raw_pr.save()
+        return self
+
+    def _get_all_comments(self) -> list[PRComment]:
+        return [
+            GitlabPRComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_pr.notes.list(sort="asc", all=True)
+        ]
+
+    def get_all_commits(self) -> list[str]:
+        return [commit.id for commit in self._raw_pr.commits()]
+
+    def comment(
+        self,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        comment = self._raw_pr.notes.create({"body": body})
+        return GitlabPRComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "PullRequest":
+        self._raw_pr.state_event = "close"
+        self._raw_pr.save()
+        return self
+
+    def merge(self) -> "PullRequest":
+        self._raw_pr.merge()
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        self._raw_pr.labels += labels
+        self._raw_pr.save()
+
+    def get_comment(self, comment_id: int) -> PRComment:
+        return GitlabPRComment(self._raw_pr.notes.get(comment_id))
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GitlabPullRequest +(raw_pr: Any, project: GitProject) +
+
+

Attributes

+
+
project : GitProject
+
Project of the pull request.
+
+
+ +Expand source code + +
class GitlabPullRequest(BasePullRequest):
+    _raw_pr: _GitlabMergeRequest
+    _target_project: "ogr_gitlab.GitlabProject"
+    _source_project: "ogr_gitlab.GitlabProject" = None
+    _merge_commit_status: ClassVar[dict[str, MergeCommitStatus]] = {
+        "can_be_merged": MergeCommitStatus.can_be_merged,
+        "cannot_be_merged": MergeCommitStatus.cannot_be_merged,
+        "unchecked": MergeCommitStatus.unchecked,
+        "checking": MergeCommitStatus.checking,
+        "cannot_be_merged_recheck": MergeCommitStatus.cannot_be_merged_recheck,
+    }
+
+    @property
+    def title(self) -> str:
+        return self._raw_pr.title
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self._raw_pr.title = new_title
+        self._raw_pr.save()
+
+    @property
+    def id(self) -> int:
+        return self._raw_pr.iid
+
+    @property
+    def status(self) -> PRStatus:
+        return (
+            PRStatus.open
+            if self._raw_pr.state == "opened"
+            else PRStatus[self._raw_pr.state]
+        )
+
+    @property
+    def url(self) -> str:
+        return self._raw_pr.web_url
+
+    @property
+    def description(self) -> str:
+        return self._raw_pr.description
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self._raw_pr.description = new_description
+        self._raw_pr.save()
+
+    @property
+    def author(self) -> str:
+        return self._raw_pr.author["username"]
+
+    @property
+    def source_branch(self) -> str:
+        return self._raw_pr.source_branch
+
+    @property
+    def target_branch(self) -> str:
+        return self._raw_pr.target_branch
+
+    @property
+    def created(self) -> datetime.datetime:
+        return self._raw_pr.created_at
+
+    @property
+    def labels(self) -> list[str]:
+        return self._raw_pr.labels
+
+    @property
+    def diff_url(self) -> str:
+        return f"{self._raw_pr.web_url}/diffs"
+
+    @property
+    def commits_url(self) -> str:
+        return f"{self._raw_pr.web_url}/commits"
+
+    @property
+    def patch(self) -> bytes:
+        response = requests.get(f"{self.url}.patch")
+
+        if not response.ok:
+            cls = OgrNetworkError if response.status_code >= 500 else GitlabAPIException
+            raise cls(
+                f"Couldn't get patch from {self.url}.patch because {response.reason}.",
+            )
+
+        return response.content
+
+    @property
+    def head_commit(self) -> str:
+        return self._raw_pr.sha
+
+    @property
+    def merge_commit_sha(self) -> Optional[str]:
+        # when merged => return merge_commit_sha
+        # otherwise => return test merge if possible
+        if self.status == PRStatus.merged:
+            return self._raw_pr.merge_commit_sha
+
+        # works for test merge only with python-gitlab>=2.10.0
+        try:
+            response = self._raw_pr.merge_ref()
+        except GitlabGetError as ex:
+            if ex.response_code == 400:
+                return None
+            raise
+        return response.get("commit_id")
+
+    @property
+    def merge_commit_status(self) -> MergeCommitStatus:
+        status = self._raw_pr.merge_status
+        if status not in self._merge_commit_status:
+            raise GitlabAPIException(f"Invalid merge_status {status}")
+
+        return self._merge_commit_status[status]
+
+    @property
+    def source_project(self) -> "ogr_gitlab.GitlabProject":
+        if self._source_project is None:
+            self._source_project = (
+                self._target_project.service.get_project_from_project_id(
+                    self._raw_pr.attributes["source_project_id"],
+                )
+            )
+        return self._source_project
+
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+    @staticmethod
+    def create(
+        project: "ogr_gitlab.GitlabProject",
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        """
+        How to create PR:
+        -  upstream -> upstream - call on upstream, fork_username unset
+        -  fork -> upstream - call on fork, fork_username unset
+           also can call on upstream with fork_username, not supported way of using
+        -  fork -> fork - call on fork, fork_username set
+        -  fork -> other_fork - call on fork, fork_username set to other_fork owner
+        """
+        repo = project.gitlab_repo
+        parameters = {
+            "source_branch": source_branch,
+            "target_branch": target_branch,
+            "title": title,
+            "description": body,
+        }
+        target_id = None
+
+        target_project = project
+        if project.is_fork and fork_username is None:
+            # handles fork -> upstream (called on fork)
+            target_id = project.parent.gitlab_repo.attributes["id"]
+            target_project = project.parent
+        elif fork_username and fork_username != project.namespace:
+            # handles fork -> upstream
+            #   (username of fork owner specified by fork_username)
+            # handles fork -> other_fork
+            #   (username of other_fork owner specified by fork_username)
+
+            other_project = GitlabPullRequest.__get_fork(
+                fork_username,
+                project if project.parent is None else project.parent,
+            )
+
+            target_id = other_project.gitlab_repo.attributes["id"]
+            if project.parent is None:
+                target_id = repo.attributes["id"]
+                repo = other_project.gitlab_repo
+        # otherwise handles PR from the same project to same project
+
+        if target_id is not None:
+            parameters["target_project_id"] = target_id
+
+        mr = repo.mergerequests.create(parameters)
+        return GitlabPullRequest(mr, target_project)
+
+    @staticmethod
+    def __get_fork(
+        fork_username: str,
+        project: "ogr_gitlab.GitlabProject",
+    ) -> "ogr_gitlab.GitlabProject":
+        """
+        Returns forked project of a requested user. Internal method, in case the fork
+        doesn't exist, raises GitlabAPIException.
+
+        Args:
+            fork_username: Username of a user that owns requested fork.
+            project: Project to search forks of.
+
+        Returns:
+            Requested fork.
+
+        Raises:
+            GitlabAPIException, in case the fork doesn't exist.
+        """
+        forks = list(
+            filter(
+                lambda fork: fork.gitlab_repo.namespace["full_path"] == fork_username,
+                project.get_forks(),
+            ),
+        )
+        if not forks:
+            raise GitlabAPIException("Requested fork doesn't exist")
+        return forks[0]
+
+    @staticmethod
+    def get(project: "ogr_gitlab.GitlabProject", pr_id: int) -> "PullRequest":
+        try:
+            mr = project.gitlab_repo.mergerequests.get(pr_id)
+        except gitlab.GitlabGetError as ex:
+            raise GitlabAPIException(f"No PR with id {pr_id} found") from ex
+        return GitlabPullRequest(mr, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_gitlab.GitlabProject",
+        status: PRStatus = PRStatus.open,
+    ) -> list["PullRequest"]:
+        # Gitlab API has status 'opened', not 'open'
+        # f"Calling a `list()` method without specifying `get_all=True` or "
+        # f"`iterator=True` will return a maximum of 20 items. "
+        mrs = project.gitlab_repo.mergerequests.list(
+            state=status.name if status != PRStatus.open else "opened",
+            order_by="updated_at",
+            sort="desc",
+            # gitlab 3.3 syntax was all=True
+            get_all=True,
+        )
+        return [GitlabPullRequest(mr, project) for mr in mrs]
+
+    def update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "PullRequest":
+        if title:
+            self._raw_pr.title = title
+        if description:
+            self._raw_pr.description = description
+
+        self._raw_pr.save()
+        return self
+
+    def _get_all_comments(self) -> list[PRComment]:
+        return [
+            GitlabPRComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in self._raw_pr.notes.list(sort="asc", all=True)
+        ]
+
+    def get_all_commits(self) -> list[str]:
+        return [commit.id for commit in self._raw_pr.commits()]
+
+    def comment(
+        self,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        comment = self._raw_pr.notes.create({"body": body})
+        return GitlabPRComment(parent=self, raw_comment=comment)
+
+    def close(self) -> "PullRequest":
+        self._raw_pr.state_event = "close"
+        self._raw_pr.save()
+        return self
+
+    def merge(self) -> "PullRequest":
+        self._raw_pr.merge()
+        return self
+
+    def add_label(self, *labels: str) -> None:
+        self._raw_pr.labels += labels
+        self._raw_pr.save()
+
+    def get_comment(self, comment_id: int) -> PRComment:
+        return GitlabPRComment(self._raw_pr.notes.get(comment_id))
+
+

Ancestors

+ +

Static methods

+
+
+def create(project: ogr_gitlab.GitlabProject, title: str, body: str, target_branch: str, source_branch: str, fork_username: Optional[str] = None) ‑> PullRequest +
+
+

How to create PR: +- +upstream -> upstream - call on upstream, fork_username unset +- +fork -> upstream - call on fork, fork_username unset +also can call on upstream with fork_username, not supported way of using +- +fork -> fork - call on fork, fork_username set +- +fork -> other_fork - call on fork, fork_username set to other_fork owner

+
+ +Expand source code + +
@staticmethod
+def create(
+    project: "ogr_gitlab.GitlabProject",
+    title: str,
+    body: str,
+    target_branch: str,
+    source_branch: str,
+    fork_username: Optional[str] = None,
+) -> "PullRequest":
+    """
+    How to create PR:
+    -  upstream -> upstream - call on upstream, fork_username unset
+    -  fork -> upstream - call on fork, fork_username unset
+       also can call on upstream with fork_username, not supported way of using
+    -  fork -> fork - call on fork, fork_username set
+    -  fork -> other_fork - call on fork, fork_username set to other_fork owner
+    """
+    repo = project.gitlab_repo
+    parameters = {
+        "source_branch": source_branch,
+        "target_branch": target_branch,
+        "title": title,
+        "description": body,
+    }
+    target_id = None
+
+    target_project = project
+    if project.is_fork and fork_username is None:
+        # handles fork -> upstream (called on fork)
+        target_id = project.parent.gitlab_repo.attributes["id"]
+        target_project = project.parent
+    elif fork_username and fork_username != project.namespace:
+        # handles fork -> upstream
+        #   (username of fork owner specified by fork_username)
+        # handles fork -> other_fork
+        #   (username of other_fork owner specified by fork_username)
+
+        other_project = GitlabPullRequest.__get_fork(
+            fork_username,
+            project if project.parent is None else project.parent,
+        )
+
+        target_id = other_project.gitlab_repo.attributes["id"]
+        if project.parent is None:
+            target_id = repo.attributes["id"]
+            repo = other_project.gitlab_repo
+    # otherwise handles PR from the same project to same project
+
+    if target_id is not None:
+        parameters["target_project_id"] = target_id
+
+    mr = repo.mergerequests.create(parameters)
+    return GitlabPullRequest(mr, target_project)
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/gitlab/release.html b/docs/services/gitlab/release.html new file mode 100644 index 00000000..91b10c3f --- /dev/null +++ b/docs/services/gitlab/release.html @@ -0,0 +1,283 @@ + + + + + + +ogr.services.gitlab.release API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.gitlab.release

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from typing import Optional
+
+from gitlab.v4.objects import ProjectRelease as _GitlabRelease
+
+from ogr.abstract import GitTag, Release
+from ogr.exceptions import OperationNotSupported
+from ogr.services import gitlab as ogr_gitlab
+
+
+class GitlabRelease(Release):
+    _raw_release: _GitlabRelease
+    project: "ogr_gitlab.GitlabProject"
+
+    @property
+    def title(self):
+        return self._raw_release.name
+
+    @property
+    def body(self):
+        return self._raw_release.description
+
+    @property
+    def git_tag(self) -> GitTag:
+        return self.project._git_tag_from_tag_name(self.tag_name)
+
+    @property
+    def tag_name(self) -> str:
+        return self._raw_release.tag_name
+
+    @property
+    def url(self) -> Optional[str]:
+        return f"{self.project.get_web_url()}/-/releases/{self.tag_name}"
+
+    @property
+    def created_at(self) -> datetime.datetime:
+        return self._raw_release.created_at
+
+    @property
+    def tarball_url(self) -> str:
+        return self._raw_release.assets["sources"][1]["url"]
+
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+    @staticmethod
+    def get(
+        project: "ogr_gitlab.GitlabProject",
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> "Release":
+        release = project.gitlab_repo.releases.get(tag_name)
+        return GitlabRelease(release, project)
+
+    @staticmethod
+    def get_latest(project: "ogr_gitlab.GitlabProject") -> Optional["Release"]:
+        releases = project.gitlab_repo.releases.list()
+        # list of releases sorted by released_at
+        return GitlabRelease(releases[0], project) if releases else None
+
+    @staticmethod
+    def get_list(project: "ogr_gitlab.GitlabProject") -> list["Release"]:
+        if not hasattr(project.gitlab_repo, "releases"):
+            raise OperationNotSupported(
+                "This version of python-gitlab does not support release, please upgrade.",
+            )
+        releases = project.gitlab_repo.releases.list(all=True)
+        return [GitlabRelease(release, project) for release in releases]
+
+    @staticmethod
+    def create(
+        project: "ogr_gitlab.GitlabProject",
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> "Release":
+        release = project.gitlab_repo.releases.create(
+            {"name": name, "tag_name": tag, "description": message, "ref": ref},
+        )
+        return GitlabRelease(release, project)
+
+    def edit_release(self, name: str, message: str) -> None:
+        raise OperationNotSupported("edit_release not supported on GitLab")
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GitlabRelease +(raw_release: Any, project: GitProject) +
+
+

Object that represents release.

+

Attributes

+
+
project : GitProject
+
Project on which the release is created.
+
+
+ +Expand source code + +
class GitlabRelease(Release):
+    _raw_release: _GitlabRelease
+    project: "ogr_gitlab.GitlabProject"
+
+    @property
+    def title(self):
+        return self._raw_release.name
+
+    @property
+    def body(self):
+        return self._raw_release.description
+
+    @property
+    def git_tag(self) -> GitTag:
+        return self.project._git_tag_from_tag_name(self.tag_name)
+
+    @property
+    def tag_name(self) -> str:
+        return self._raw_release.tag_name
+
+    @property
+    def url(self) -> Optional[str]:
+        return f"{self.project.get_web_url()}/-/releases/{self.tag_name}"
+
+    @property
+    def created_at(self) -> datetime.datetime:
+        return self._raw_release.created_at
+
+    @property
+    def tarball_url(self) -> str:
+        return self._raw_release.assets["sources"][1]["url"]
+
+    def __str__(self) -> str:
+        return "Gitlab" + super().__str__()
+
+    @staticmethod
+    def get(
+        project: "ogr_gitlab.GitlabProject",
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> "Release":
+        release = project.gitlab_repo.releases.get(tag_name)
+        return GitlabRelease(release, project)
+
+    @staticmethod
+    def get_latest(project: "ogr_gitlab.GitlabProject") -> Optional["Release"]:
+        releases = project.gitlab_repo.releases.list()
+        # list of releases sorted by released_at
+        return GitlabRelease(releases[0], project) if releases else None
+
+    @staticmethod
+    def get_list(project: "ogr_gitlab.GitlabProject") -> list["Release"]:
+        if not hasattr(project.gitlab_repo, "releases"):
+            raise OperationNotSupported(
+                "This version of python-gitlab does not support release, please upgrade.",
+            )
+        releases = project.gitlab_repo.releases.list(all=True)
+        return [GitlabRelease(release, project) for release in releases]
+
+    @staticmethod
+    def create(
+        project: "ogr_gitlab.GitlabProject",
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> "Release":
+        release = project.gitlab_repo.releases.create(
+            {"name": name, "tag_name": tag, "description": message, "ref": ref},
+        )
+        return GitlabRelease(release, project)
+
+    def edit_release(self, name: str, message: str) -> None:
+        raise OperationNotSupported("edit_release not supported on GitLab")
+
+

Ancestors

+ +

Class variables

+
+
var projectGitlabProject
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/gitlab/service.html b/docs/services/gitlab/service.html new file mode 100644 index 00000000..543f6b02 --- /dev/null +++ b/docs/services/gitlab/service.html @@ -0,0 +1,491 @@ + + + + + + +ogr.services.gitlab.service API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.gitlab.service

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import logging
+from typing import Optional
+
+import gitlab
+
+from ogr.abstract import GitUser
+from ogr.exceptions import GitlabAPIException, OperationNotSupported
+from ogr.factory import use_for_service
+from ogr.services.base import BaseGitService, GitProject
+from ogr.services.gitlab.project import GitlabProject
+from ogr.services.gitlab.user import GitlabUser
+
+logger = logging.getLogger(__name__)
+
+
+@use_for_service("gitlab")  # anything containing a gitlab word in hostname
+# + list of community-hosted instances based on the following list
+# https://wiki.p2pfoundation.net/List_of_Community-Hosted_GitLab_Instances
+@use_for_service("salsa.debian.org")
+@use_for_service("git.fosscommunity.in")
+@use_for_service("framagit.org")
+@use_for_service("dev.gajim.org")
+@use_for_service("git.coop")
+@use_for_service("lab.libreho.st")
+@use_for_service("git.linux-kernel.at")
+@use_for_service("git.pleroma.social")
+@use_for_service("git.silence.dev")
+@use_for_service("code.videolan.org")
+@use_for_service("source.puri.sm")
+class GitlabService(BaseGitService):
+    name = "gitlab"
+
+    def __init__(self, token=None, instance_url=None, ssl_verify=True, **kwargs):
+        super().__init__(token=token)
+        self.instance_url = instance_url or "https://gitlab.com"
+        self.token = token
+        self.ssl_verify = ssl_verify
+        self._gitlab_instance = None
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    @property
+    def gitlab_instance(self) -> gitlab.Gitlab:
+        if not self._gitlab_instance:
+            self._gitlab_instance = gitlab.Gitlab(
+                url=self.instance_url,
+                private_token=self.token,
+                ssl_verify=self.ssl_verify,
+            )
+            if self.token:
+                self._gitlab_instance.auth()
+        return self._gitlab_instance
+
+    @property
+    def user(self) -> GitUser:
+        return GitlabUser(service=self)
+
+    def __str__(self) -> str:
+        token_str = (
+            f", token='{self.token[:1]}***{self.token[-1:]}'" if self.token else ""
+        )
+        ssl_str = ", ssl_verify=False" if not self.ssl_verify else ""
+        return (
+            f"GitlabService(instance_url='{self.instance_url}'"
+            f"{token_str}"
+            f"{ssl_str})"
+        )
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, GitlabService):
+            return False
+
+        return (
+            self.token == o.token  # type: ignore
+            and self.instance_url == o.instance_url  # type: ignore
+            and self.ssl_verify == o.ssl_verify  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(
+        self,
+        repo=None,
+        namespace=None,
+        is_fork=False,
+        **kwargs,
+    ) -> "GitlabProject":
+        if is_fork:
+            namespace = self.user.get_username()
+        return GitlabProject(repo=repo, namespace=namespace, service=self, **kwargs)
+
+    def get_project_from_project_id(self, iid: int) -> "GitlabProject":
+        gitlab_repo = self.gitlab_instance.projects.get(iid)
+        return GitlabProject(
+            repo=gitlab_repo.attributes["path"],
+            namespace=gitlab_repo.attributes["namespace"]["full_path"],
+            service=self,
+            gitlab_repo=gitlab_repo,
+        )
+
+    def change_token(self, new_token: str) -> None:
+        self.token = new_token
+        self._gitlab_instance = None
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "GitlabProject":
+        data = {"name": repo}
+        if namespace:
+            try:
+                group = self.gitlab_instance.groups.get(namespace)
+            except gitlab.GitlabGetError as ex:
+                raise GitlabAPIException(f"Group {namespace} not found.") from ex
+            data["namespace_id"] = group.id
+
+        if description:
+            data["description"] = description
+        try:
+            new_project = self.gitlab_instance.projects.create(data)
+        except gitlab.GitlabCreateError as ex:
+            raise GitlabAPIException("Project already exists") from ex
+        return GitlabProject(
+            repo=repo,
+            namespace=namespace,
+            service=self,
+            gitlab_repo=new_project,
+        )
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        if namespace:
+            group = self.gitlab_instance.groups.get(namespace)
+            projects = group.projects.list(all=True)
+        elif user:
+            user_object = self.gitlab_instance.users.list(username=user)[0]
+            projects = user_object.projects.list(all=True)
+        else:
+            raise OperationNotSupported
+
+        if language:
+            # group.projects.list gives us a GroupProject instance
+            # in order to be able to filter by language we need Project instance
+            projects_to_convert = [
+                self.gitlab_instance.projects.get(item.attributes["id"])
+                for item in projects
+                if language
+                in self.gitlab_instance.projects.get(item.attributes["id"]).languages()
+            ]
+        else:
+            projects_to_convert = projects
+        return [
+            GitlabProject(
+                repo=project.attributes["path"],
+                namespace=project.attributes["namespace"]["full_path"],
+                service=self,
+            )
+            for project in projects_to_convert
+        ]
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GitlabService +(token=None, instance_url=None, ssl_verify=True, **kwargs) +
+
+

Attributes

+
+
instance_url : str
+
URL of the git forge instance.
+
+
+ +Expand source code + +
@use_for_service("gitlab")  # anything containing a gitlab word in hostname
+# + list of community-hosted instances based on the following list
+# https://wiki.p2pfoundation.net/List_of_Community-Hosted_GitLab_Instances
+@use_for_service("salsa.debian.org")
+@use_for_service("git.fosscommunity.in")
+@use_for_service("framagit.org")
+@use_for_service("dev.gajim.org")
+@use_for_service("git.coop")
+@use_for_service("lab.libreho.st")
+@use_for_service("git.linux-kernel.at")
+@use_for_service("git.pleroma.social")
+@use_for_service("git.silence.dev")
+@use_for_service("code.videolan.org")
+@use_for_service("source.puri.sm")
+class GitlabService(BaseGitService):
+    name = "gitlab"
+
+    def __init__(self, token=None, instance_url=None, ssl_verify=True, **kwargs):
+        super().__init__(token=token)
+        self.instance_url = instance_url or "https://gitlab.com"
+        self.token = token
+        self.ssl_verify = ssl_verify
+        self._gitlab_instance = None
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    @property
+    def gitlab_instance(self) -> gitlab.Gitlab:
+        if not self._gitlab_instance:
+            self._gitlab_instance = gitlab.Gitlab(
+                url=self.instance_url,
+                private_token=self.token,
+                ssl_verify=self.ssl_verify,
+            )
+            if self.token:
+                self._gitlab_instance.auth()
+        return self._gitlab_instance
+
+    @property
+    def user(self) -> GitUser:
+        return GitlabUser(service=self)
+
+    def __str__(self) -> str:
+        token_str = (
+            f", token='{self.token[:1]}***{self.token[-1:]}'" if self.token else ""
+        )
+        ssl_str = ", ssl_verify=False" if not self.ssl_verify else ""
+        return (
+            f"GitlabService(instance_url='{self.instance_url}'"
+            f"{token_str}"
+            f"{ssl_str})"
+        )
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, GitlabService):
+            return False
+
+        return (
+            self.token == o.token  # type: ignore
+            and self.instance_url == o.instance_url  # type: ignore
+            and self.ssl_verify == o.ssl_verify  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(
+        self,
+        repo=None,
+        namespace=None,
+        is_fork=False,
+        **kwargs,
+    ) -> "GitlabProject":
+        if is_fork:
+            namespace = self.user.get_username()
+        return GitlabProject(repo=repo, namespace=namespace, service=self, **kwargs)
+
+    def get_project_from_project_id(self, iid: int) -> "GitlabProject":
+        gitlab_repo = self.gitlab_instance.projects.get(iid)
+        return GitlabProject(
+            repo=gitlab_repo.attributes["path"],
+            namespace=gitlab_repo.attributes["namespace"]["full_path"],
+            service=self,
+            gitlab_repo=gitlab_repo,
+        )
+
+    def change_token(self, new_token: str) -> None:
+        self.token = new_token
+        self._gitlab_instance = None
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "GitlabProject":
+        data = {"name": repo}
+        if namespace:
+            try:
+                group = self.gitlab_instance.groups.get(namespace)
+            except gitlab.GitlabGetError as ex:
+                raise GitlabAPIException(f"Group {namespace} not found.") from ex
+            data["namespace_id"] = group.id
+
+        if description:
+            data["description"] = description
+        try:
+            new_project = self.gitlab_instance.projects.create(data)
+        except gitlab.GitlabCreateError as ex:
+            raise GitlabAPIException("Project already exists") from ex
+        return GitlabProject(
+            repo=repo,
+            namespace=namespace,
+            service=self,
+            gitlab_repo=new_project,
+        )
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        if namespace:
+            group = self.gitlab_instance.groups.get(namespace)
+            projects = group.projects.list(all=True)
+        elif user:
+            user_object = self.gitlab_instance.users.list(username=user)[0]
+            projects = user_object.projects.list(all=True)
+        else:
+            raise OperationNotSupported
+
+        if language:
+            # group.projects.list gives us a GroupProject instance
+            # in order to be able to filter by language we need Project instance
+            projects_to_convert = [
+                self.gitlab_instance.projects.get(item.attributes["id"])
+                for item in projects
+                if language
+                in self.gitlab_instance.projects.get(item.attributes["id"]).languages()
+            ]
+        else:
+            projects_to_convert = projects
+        return [
+            GitlabProject(
+                repo=project.attributes["path"],
+                namespace=project.attributes["namespace"]["full_path"],
+                service=self,
+            )
+            for project in projects_to_convert
+        ]
+
+

Ancestors

+ +

Class variables

+
+
var name
+
+
+
+
+

Instance variables

+
+
var gitlab_instance : gitlab.client.Gitlab
+
+
+
+ +Expand source code + +
@property
+def gitlab_instance(self) -> gitlab.Gitlab:
+    if not self._gitlab_instance:
+        self._gitlab_instance = gitlab.Gitlab(
+            url=self.instance_url,
+            private_token=self.token,
+            ssl_verify=self.ssl_verify,
+        )
+        if self.token:
+            self._gitlab_instance.auth()
+    return self._gitlab_instance
+
+
+
+

Methods

+
+
+def get_project_from_project_id(self, iid: int) ‑> GitlabProject +
+
+
+
+ +Expand source code + +
def get_project_from_project_id(self, iid: int) -> "GitlabProject":
+    gitlab_repo = self.gitlab_instance.projects.get(iid)
+    return GitlabProject(
+        repo=gitlab_repo.attributes["path"],
+        namespace=gitlab_repo.attributes["namespace"]["full_path"],
+        service=self,
+        gitlab_repo=gitlab_repo,
+    )
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/gitlab/user.html b/docs/services/gitlab/user.html new file mode 100644 index 00000000..76f4a376 --- /dev/null +++ b/docs/services/gitlab/user.html @@ -0,0 +1,164 @@ + + + + + + +ogr.services.gitlab.user API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.gitlab.user

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+
+from ogr.exceptions import OperationNotSupported
+from ogr.services import gitlab as ogr_gitlab
+from ogr.services.base import BaseGitUser
+
+
+class GitlabUser(BaseGitUser):
+    service: "ogr_gitlab.GitlabService"
+
+    def __init__(self, service: "ogr_gitlab.GitlabService") -> None:
+        super().__init__(service=service)
+
+    def __str__(self) -> str:
+        return f'GitlabUser(username="{self.get_username()}")'
+
+    @property
+    def _gitlab_user(self):
+        return self.service.gitlab_instance.user
+
+    def get_username(self) -> str:
+        return self._gitlab_user.username
+
+    def get_email(self) -> str:
+        return self._gitlab_user.email
+
+    def get_projects(self) -> list["ogr_gitlab.GitlabProject"]:
+        raise OperationNotSupported
+
+    def get_forks(self) -> list["ogr_gitlab.GitlabProject"]:
+        raise OperationNotSupported
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GitlabUser +(service: ogr_gitlab.GitlabService) +
+
+

Represents currently authenticated user through service.

+
+ +Expand source code + +
class GitlabUser(BaseGitUser):
+    service: "ogr_gitlab.GitlabService"
+
+    def __init__(self, service: "ogr_gitlab.GitlabService") -> None:
+        super().__init__(service=service)
+
+    def __str__(self) -> str:
+        return f'GitlabUser(username="{self.get_username()}")'
+
+    @property
+    def _gitlab_user(self):
+        return self.service.gitlab_instance.user
+
+    def get_username(self) -> str:
+        return self._gitlab_user.username
+
+    def get_email(self) -> str:
+        return self._gitlab_user.email
+
+    def get_projects(self) -> list["ogr_gitlab.GitlabProject"]:
+        raise OperationNotSupported
+
+    def get_forks(self) -> list["ogr_gitlab.GitlabProject"]:
+        raise OperationNotSupported
+
+

Ancestors

+ +

Class variables

+
+
var serviceGitlabService
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/index.html b/docs/services/index.html new file mode 100644 index 00000000..67450957 --- /dev/null +++ b/docs/services/index.html @@ -0,0 +1,87 @@ + + + + + + +ogr.services API documentation + + + + + + + + + + + +
+ + +
+ + + \ No newline at end of file diff --git a/docs/services/pagure/comments.html b/docs/services/pagure/comments.html new file mode 100644 index 00000000..0e2981ee --- /dev/null +++ b/docs/services/pagure/comments.html @@ -0,0 +1,251 @@ + + + + + + +ogr.services.pagure.comments API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.pagure.comments

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from typing import Any, Optional
+
+from ogr.abstract import Comment, IssueComment, PRComment
+from ogr.exceptions import OperationNotSupported
+
+
+class PagureComment(Comment):
+    def _from_raw_comment(self, raw_comment: dict[str, Any]) -> None:
+        self._body = raw_comment["comment"]
+        self._id = raw_comment["id"]
+        self._author = raw_comment["user"]["name"]
+        self._created = self.__datetime_from_timestamp(raw_comment["date_created"])
+        self._edited = self.__datetime_from_timestamp(raw_comment["edited_on"])
+
+    @staticmethod
+    def __datetime_from_timestamp(
+        timestamp: Optional[str],
+    ) -> Optional[datetime.datetime]:
+        return datetime.datetime.fromtimestamp(int(timestamp)) if timestamp else None
+
+    @property
+    def body(self) -> str:
+        return self._body
+
+    @body.setter
+    def body(self, new_body: str) -> None:
+        raise OperationNotSupported()
+
+
+class PagureIssueComment(PagureComment, IssueComment):
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+
+class PagurePRComment(PagureComment, PRComment):
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class PagureComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class PagureComment(Comment):
+    def _from_raw_comment(self, raw_comment: dict[str, Any]) -> None:
+        self._body = raw_comment["comment"]
+        self._id = raw_comment["id"]
+        self._author = raw_comment["user"]["name"]
+        self._created = self.__datetime_from_timestamp(raw_comment["date_created"])
+        self._edited = self.__datetime_from_timestamp(raw_comment["edited_on"])
+
+    @staticmethod
+    def __datetime_from_timestamp(
+        timestamp: Optional[str],
+    ) -> Optional[datetime.datetime]:
+        return datetime.datetime.fromtimestamp(int(timestamp)) if timestamp else None
+
+    @property
+    def body(self) -> str:
+        return self._body
+
+    @body.setter
+    def body(self, new_body: str) -> None:
+        raise OperationNotSupported()
+
+

Ancestors

+ +

Subclasses

+ +

Inherited members

+ +
+
+class PagureIssueComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class PagureIssueComment(PagureComment, IssueComment):
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+class PagurePRComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class PagurePRComment(PagureComment, PRComment):
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/pagure/flag.html b/docs/services/pagure/flag.html new file mode 100644 index 00000000..553024ab --- /dev/null +++ b/docs/services/pagure/flag.html @@ -0,0 +1,266 @@ + + + + + + +ogr.services.pagure.flag API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.pagure.flag

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from typing import Any, ClassVar, Optional
+
+from ogr.abstract import CommitFlag, CommitStatus
+from ogr.services import pagure as ogr_pagure
+from ogr.services.base import BaseCommitFlag
+
+
+class PagureCommitFlag(BaseCommitFlag):
+    _states: ClassVar[dict[str, CommitStatus]] = {
+        "pending": CommitStatus.pending,
+        "success": CommitStatus.success,
+        "failure": CommitStatus.failure,
+        "error": CommitStatus.error,
+        "canceled": CommitStatus.canceled,
+    }
+
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+    def _from_raw_commit_flag(self):
+        self.commit = self._raw_commit_flag["commit_hash"]
+        self.comment = self._raw_commit_flag["comment"]
+        self.state = self._state_from_str(self._raw_commit_flag["status"])
+        self.context = self._raw_commit_flag["username"]
+        self.url = self._raw_commit_flag["url"]
+
+    @staticmethod
+    def get(project: "ogr_pagure.PagureProject", commit: str) -> list["CommitFlag"]:
+        response = project._call_project_api("c", commit, "flag")
+        return [
+            PagureCommitFlag(raw_commit_flag=flag, project=project)
+            for flag in response["flags"]
+        ]
+
+    @staticmethod
+    def set(
+        project: "ogr_pagure.PagureProject",
+        commit: str,
+        state: CommitStatus,
+        target_url: str,
+        description: str,
+        context: str,
+        percent: Optional[int] = None,
+        trim: bool = False,
+        uid: Optional[str] = None,
+    ) -> "CommitFlag":
+        state = PagureCommitFlag._validate_state(state)
+
+        if trim:
+            description = description[:140]
+
+        data: dict[str, Any] = {
+            "username": context,
+            "comment": description,
+            "url": target_url,
+            "status": state.name,
+        }
+        if percent:
+            data["percent"] = percent
+        if uid:
+            data["uid"] = uid
+
+        response = project._call_project_api(
+            "c",
+            commit,
+            "flag",
+            method="POST",
+            data=data,
+        )
+        return PagureCommitFlag(
+            project=project,
+            raw_commit_flag=response["flag"],
+            uid=response["uid"],
+        )
+
+    @property
+    def created(self) -> datetime.datetime:
+        return datetime.datetime.fromtimestamp(
+            int(self._raw_commit_flag["date_created"]),
+        )
+
+    @property
+    def edited(self) -> datetime.datetime:
+        return datetime.datetime.fromtimestamp(
+            int(self._raw_commit_flag["date_updated"]),
+        )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class PagureCommitFlag +(raw_commit_flag: Optional[Any] = None, project: Optional[ForwardRef('GitProject')] = None, commit: Optional[str] = None, state: Optional[CommitStatus] = None, context: Optional[str] = None, comment: Optional[str] = None, uid: Optional[str] = None, url: Optional[str] = None) +
+
+
+
+ +Expand source code + +
class PagureCommitFlag(BaseCommitFlag):
+    _states: ClassVar[dict[str, CommitStatus]] = {
+        "pending": CommitStatus.pending,
+        "success": CommitStatus.success,
+        "failure": CommitStatus.failure,
+        "error": CommitStatus.error,
+        "canceled": CommitStatus.canceled,
+    }
+
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+    def _from_raw_commit_flag(self):
+        self.commit = self._raw_commit_flag["commit_hash"]
+        self.comment = self._raw_commit_flag["comment"]
+        self.state = self._state_from_str(self._raw_commit_flag["status"])
+        self.context = self._raw_commit_flag["username"]
+        self.url = self._raw_commit_flag["url"]
+
+    @staticmethod
+    def get(project: "ogr_pagure.PagureProject", commit: str) -> list["CommitFlag"]:
+        response = project._call_project_api("c", commit, "flag")
+        return [
+            PagureCommitFlag(raw_commit_flag=flag, project=project)
+            for flag in response["flags"]
+        ]
+
+    @staticmethod
+    def set(
+        project: "ogr_pagure.PagureProject",
+        commit: str,
+        state: CommitStatus,
+        target_url: str,
+        description: str,
+        context: str,
+        percent: Optional[int] = None,
+        trim: bool = False,
+        uid: Optional[str] = None,
+    ) -> "CommitFlag":
+        state = PagureCommitFlag._validate_state(state)
+
+        if trim:
+            description = description[:140]
+
+        data: dict[str, Any] = {
+            "username": context,
+            "comment": description,
+            "url": target_url,
+            "status": state.name,
+        }
+        if percent:
+            data["percent"] = percent
+        if uid:
+            data["uid"] = uid
+
+        response = project._call_project_api(
+            "c",
+            commit,
+            "flag",
+            method="POST",
+            data=data,
+        )
+        return PagureCommitFlag(
+            project=project,
+            raw_commit_flag=response["flag"],
+            uid=response["uid"],
+        )
+
+    @property
+    def created(self) -> datetime.datetime:
+        return datetime.datetime.fromtimestamp(
+            int(self._raw_commit_flag["date_created"]),
+        )
+
+    @property
+    def edited(self) -> datetime.datetime:
+        return datetime.datetime.fromtimestamp(
+            int(self._raw_commit_flag["date_updated"]),
+        )
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/pagure/index.html b/docs/services/pagure/index.html new file mode 100644 index 00000000..f381730f --- /dev/null +++ b/docs/services/pagure/index.html @@ -0,0 +1,2847 @@ + + + + + + +ogr.services.pagure API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.pagure

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+from ogr.services.pagure.comments import PagureIssueComment, PagurePRComment
+from ogr.services.pagure.issue import PagureIssue
+from ogr.services.pagure.project import PagureProject
+from ogr.services.pagure.pull_request import PagurePullRequest
+from ogr.services.pagure.release import PagureRelease
+from ogr.services.pagure.service import PagureService
+from ogr.services.pagure.user import PagureUser
+
+__all__ = [
+    PagurePullRequest.__name__,
+    PagureIssueComment.__name__,
+    PagurePRComment.__name__,
+    PagureIssue.__name__,
+    PagureRelease.__name__,
+    PagureUser.__name__,
+    PagureProject.__name__,
+    PagureService.__name__,
+]
+
+
+
+

Sub-modules

+
+
ogr.services.pagure.comments
+
+
+
+
ogr.services.pagure.flag
+
+
+
+
ogr.services.pagure.issue
+
+
+
+
ogr.services.pagure.project
+
+
+
+
ogr.services.pagure.pull_request
+
+
+
+
ogr.services.pagure.release
+
+
+
+
ogr.services.pagure.service
+
+
+
+
ogr.services.pagure.user
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class PagureIssue +(raw_issue, project) +
+
+

Attributes

+
+
project : GitProject
+
Project of the issue.
+
+
+ +Expand source code + +
class PagureIssue(BaseIssue):
+    project: "ogr_pagure.PagureProject"
+
+    def __init__(self, raw_issue, project):
+        super().__init__(raw_issue, project)
+        self.__dirty = False
+
+    def __update(self):
+        if self.__dirty:
+            self._raw_issue = self.project._call_project_api("issue", str(self.id))
+            self.__dirty = False
+
+    @property
+    def title(self) -> str:
+        self.__update()
+        return self._raw_issue["title"]
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self.__update_info(title=new_title)
+
+    @property
+    def private(self) -> bool:
+        self.__update()
+        return self._raw_issue["private"]
+
+    @property
+    def id(self) -> int:
+        return self._raw_issue["id"]
+
+    @property
+    def status(self) -> IssueStatus:
+        self.__update()
+        return IssueStatus[self._raw_issue["status"].lower()]
+
+    @property
+    def url(self) -> str:
+        return self.project._get_project_url(
+            "issue",
+            str(self.id),
+            add_api_endpoint_part=False,
+        )
+
+    @property
+    def assignee(self) -> str:
+        self.__update()
+        try:
+            return self._raw_issue["assignee"]["name"]
+        except Exception:
+            return None
+
+    @property
+    def description(self) -> str:
+        self.__update()
+        return self._raw_issue["content"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.__update_info(description=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_issue["user"]["name"]
+
+    @property
+    def created(self) -> datetime.datetime:
+        return datetime.datetime.fromtimestamp(int(self._raw_issue["date_created"]))
+
+    @property
+    def labels(self) -> list[str]:
+        return self._raw_issue["tags"]
+
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+    def __update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        assignee: Optional[str] = None,
+    ) -> None:
+        try:
+            data = {
+                "title": title if title is not None else self.title,
+                "issue_content": description
+                if description is not None
+                else self.description,
+            }
+
+            updated_issue = self.project._call_project_api(
+                "issue",
+                str(self.id),
+                method="POST",
+                data=data,
+            )
+            self._raw_issue = updated_issue["issue"]
+        except Exception as ex:
+            raise PagureAPIException(
+                "there was an error while updating the issue",
+            ) from ex
+
+    @staticmethod
+    def create(
+        project: "ogr_pagure.PagureProject",
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        payload = {"title": title, "issue_content": body}
+        if labels is not None:
+            payload["tag"] = ",".join(labels)
+        if private:
+            payload["private"] = "true"
+        if assignees and len(assignees) > 1:
+            raise OperationNotSupported("Pagure does not support multiple assignees")
+
+        if assignees:
+            payload["assignee"] = assignees[0]
+
+        new_issue = project._call_project_api("new_issue", data=payload, method="POST")[
+            "issue"
+        ]
+        return PagureIssue(new_issue, project)
+
+    @staticmethod
+    def get(project: "ogr_pagure.PagureProject", issue_id: int) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        raw_issue = project._call_project_api("issue", str(issue_id))
+        return PagureIssue(raw_issue, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_pagure.PagureProject",
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        payload: dict[str, Union[str, list[str], int]] = {
+            "status": status.name.capitalize(),
+            "page": 1,
+            "per_page": 100,
+        }
+        if author:
+            payload["author"] = author
+        if assignee:
+            payload["assignee"] = assignee
+        if labels:
+            payload["tags"] = labels
+
+        raw_issues: list[Any] = []
+
+        while True:
+            issues_info = project._call_project_api("issues", params=payload)
+            raw_issues += issues_info["issues"]
+            if not issues_info["pagination"]["next"]:
+                break
+            payload["page"] = cast(int, payload["page"]) + 1
+
+        return [PagureIssue(issue_dict, project) for issue_dict in raw_issues]
+
+    def _get_all_comments(self) -> list[IssueComment]:
+        self.__update()
+        raw_comments = self._raw_issue["comments"]
+        return [
+            PagureIssueComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in raw_comments
+        ]
+
+    def comment(self, body: str) -> IssueComment:
+        payload = {"comment": body}
+        self.project._call_project_api(
+            "issue",
+            str(self.id),
+            "comment",
+            data=payload,
+            method="POST",
+        )
+        self.__dirty = True
+        return PagureIssueComment(parent=self, body=body, author=self.project._user)
+
+    def close(self) -> "PagureIssue":
+        payload = {"status": "Closed"}
+        self.project._call_project_api(
+            "issue",
+            str(self.id),
+            "status",
+            data=payload,
+            method="POST",
+        )
+        self.__dirty = True
+        return self
+
+    def add_assignee(self, *assignees: str) -> None:
+        if len(assignees) > 1:
+            raise OperationNotSupported("Pagure does not support multiple assignees")
+        payload = {"assignee": assignees[0]}
+        self.project._call_project_api(
+            "issue",
+            str(self.id),
+            "assign",
+            data=payload,
+            method="POST",
+        )
+
+    def get_comment(self, comment_id: int) -> IssueComment:
+        return PagureIssueComment(
+            self.project._call_project_api(
+                "issue",
+                str(self.id),
+                "comment",
+                str(comment_id),
+                method="GET",
+            ),
+        )
+
+

Ancestors

+ +

Class variables

+
+
var projectPagureProject
+
+
+
+
+

Instance variables

+
+
var assignee : str
+
+
+
+ +Expand source code + +
@property
+def assignee(self) -> str:
+    self.__update()
+    try:
+        return self._raw_issue["assignee"]["name"]
+    except Exception:
+        return None
+
+
+
+

Inherited members

+ +
+
+class PagureIssueComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class PagureIssueComment(PagureComment, IssueComment):
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+class PagurePRComment +(raw_comment: Optional[Any] = None, parent: Optional[Any] = None, body: Optional[str] = None, id_: Optional[int] = None, author: Optional[str] = None, created: Optional[datetime.datetime] = None, edited: Optional[datetime.datetime] = None) +
+
+
+
+ +Expand source code + +
class PagurePRComment(PagureComment, PRComment):
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+

Ancestors

+ +

Inherited members

+ +
+
+class PagureProject +(repo: str, namespace: Optional[str], service: ogr_pagure.PagureService, username: Optional[str] = None, is_fork: bool = False) +
+
+

Args

+
+
repo
+
Name of the project.
+
service
+
GitService instance.
+
namespace
+
+

Namespace of the project.

+
    +
  • GitHub: username or org name.
  • +
  • GitLab: username or org name.
  • +
  • Pagure: namespace (e.g. "rpms").
  • +
+

In case of forks: "fork/{username}/{namespace}".

+
+
+
+ +Expand source code + +
class PagureProject(BaseGitProject):
+    service: "ogr_pagure.PagureService"
+
+    def __init__(
+        self,
+        repo: str,
+        namespace: Optional[str],
+        service: "ogr_pagure.PagureService",
+        username: Optional[str] = None,
+        is_fork: bool = False,
+    ) -> None:
+        super().__init__(repo, service, namespace)
+        self.read_only = service.read_only
+
+        self._is_fork = is_fork
+        self._username = username
+
+        self.repo = repo
+        self.namespace = namespace
+
+    def __str__(self) -> str:
+        fork_info = ""
+        if self._is_fork:
+            fork_info = f', username="{self._username}", is_fork={self._is_fork}'
+        return f'PagureProject(namespace="{self.namespace}", repo="{self.repo}"{fork_info})'
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, PagureProject):
+            return False
+
+        return (
+            self.repo == o.repo
+            and self.namespace == o.namespace
+            and self.service == o.service
+            and self._username == o._username
+            and self._is_fork == o._is_fork
+            and self.read_only == o.read_only
+        )
+
+    @property
+    def _user(self) -> str:
+        if not self._username:
+            self._username = self.service.user.get_username()
+        return self._username
+
+    def _call_project_api(
+        self,
+        *args,
+        add_fork_part: bool = True,
+        add_api_endpoint_part: bool = True,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data: Optional[dict] = None,
+    ) -> dict:
+        """
+        Call project API endpoint.
+
+        Args:
+            *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+            add_fork_part: If the project is a fork, use `fork/username` prefix.
+
+                Defaults to `True`.
+            add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+                Defaults to `True`.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            Dictionary representing response.
+        """
+        request_url = self._get_project_url(
+            *args,
+            add_api_endpoint_part=add_api_endpoint_part,
+            add_fork_part=add_fork_part,
+        )
+
+        return self.service.call_api(
+            url=request_url,
+            method=method,
+            params=params,
+            data=data,
+        )
+
+    def _call_project_api_raw(
+        self,
+        *args,
+        add_fork_part: bool = True,
+        add_api_endpoint_part: bool = True,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data: Optional[dict] = None,
+    ) -> RequestResponse:
+        """
+        Call project API endpoint.
+
+        Args:
+            *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+            add_fork_part: If the project is a fork, use `fork/username` prefix.
+
+                Defaults to `True`.
+            add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+                Defaults to `True`.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            `RequestResponse` object containing response.
+        """
+        request_url = self._get_project_url(
+            *args,
+            add_api_endpoint_part=add_api_endpoint_part,
+            add_fork_part=add_fork_part,
+        )
+
+        return self.service.call_api_raw(
+            url=request_url,
+            method=method,
+            params=params,
+            data=data,
+        )
+
+    def _get_project_url(self, *args, add_fork_part=True, add_api_endpoint_part=True):
+        additional_parts = []
+        if self._is_fork and add_fork_part:
+            additional_parts += ["fork", self._user]
+        return self.service.get_api_url(
+            *additional_parts,
+            self.namespace,
+            self.repo,
+            *args,
+            add_api_endpoint_part=add_api_endpoint_part,
+        )
+
+    def get_project_info(self):
+        return self._call_project_api(method="GET")
+
+    def get_branches(self) -> list[str]:
+        return_value = self._call_project_api("git", "branches", method="GET")
+        return return_value["branches"]
+
+    @property
+    def default_branch(self) -> str:
+        return_value = self._call_project_api("git", "branches", method="GET")
+        return return_value["default"]
+
+    def get_description(self) -> str:
+        return self.get_project_info()["description"]
+
+    @property
+    def description(self) -> str:
+        return self.get_project_info()["description"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        raise OperationNotSupported("Not possible on Pagure")
+
+    @property
+    def has_issues(self) -> bool:
+        options = self._call_project_api("options", method="GET")
+        return options["settings"]["issue_tracker"]
+
+    def get_owners(self) -> list[str]:
+        project = self.get_project_info()
+        return project["access_users"]["owner"]
+
+    def who_can_close_issue(self) -> set[str]:
+        users: set[str] = set()
+        project = self.get_project_info()
+        users.update(project["access_users"]["admin"])
+        users.update(project["access_users"]["commit"])
+        users.update(project["access_users"]["ticket"])
+        users.update(project["access_users"]["owner"])
+        return users
+
+    def who_can_merge_pr(self) -> set[str]:
+        users: set[str] = set()
+        project = self.get_project_info()
+        users.update(project["access_users"]["admin"])
+        users.update(project["access_users"]["commit"])
+        users.update(project["access_users"]["owner"])
+        return users
+
+    def which_groups_can_merge_pr(self) -> set[str]:
+        groups: set[str] = set()
+        project = self.get_project_info()
+        groups.update(project["access_groups"]["admin"])
+        groups.update(project["access_groups"]["commit"])
+        return groups
+
+    def can_merge_pr(self, username) -> bool:
+        return username in self.who_can_merge_pr()
+
+    def request_access(self):
+        raise OperationNotSupported("Not possible on Pagure")
+
+    @indirect(PagureIssue.get_list)
+    def get_issue_list(
+        self,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list[Issue]:
+        pass
+
+    @indirect(PagureIssue.get)
+    def get_issue(self, issue_id: int) -> Issue:
+        pass
+
+    def delete(self) -> None:
+        self._call_project_api_raw("delete", method="POST")
+
+    @indirect(PagureIssue.create)
+    def create_issue(
+        self,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> Issue:
+        pass
+
+    @indirect(PagurePullRequest.get_list)
+    def get_pr_list(
+        self,
+        status: PRStatus = PRStatus.open,
+        assignee=None,
+        author=None,
+    ) -> list[PullRequest]:
+        pass
+
+    @indirect(PagurePullRequest.get)
+    def get_pr(self, pr_id: int) -> PullRequest:
+        pass
+
+    @indirect(PagurePullRequest.get_files_diff)
+    def get_pr_files_diff(
+        self,
+        pr_id: int,
+        retries: int = 0,
+        wait_seconds: int = 3,
+    ) -> dict:
+        pass
+
+    @if_readonly(return_function=GitProjectReadOnly.create_pr)
+    @indirect(PagurePullRequest.create)
+    def create_pr(
+        self,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> PullRequest:
+        pass
+
+    @if_readonly(return_function=GitProjectReadOnly.fork_create)
+    def fork_create(self, namespace: Optional[str] = None) -> "PagureProject":
+        if namespace is not None:
+            raise OperationNotSupported(
+                "Pagure does not support forking to namespaces.",
+            )
+
+        request_url = self.service.get_api_url("fork")
+        self.service.call_api(
+            url=request_url,
+            method="POST",
+            data={"repo": self.repo, "namespace": self.namespace, "wait": True},
+        )
+        fork = self._construct_fork_project()
+        logger.debug(f"Forked to {fork.full_repo_name}")
+        return fork
+
+    def _construct_fork_project(self) -> "PagureProject":
+        return PagureProject(
+            service=self.service,
+            repo=self.repo,
+            namespace=self.namespace,
+            username=self._user,
+            is_fork=True,
+        )
+
+    def get_fork(self, create: bool = True) -> Optional["PagureProject"]:
+        if self.is_fork:
+            raise OgrException("Cannot create fork from fork.")
+
+        for fork in self.get_forks():
+            fork_info = fork.get_project_info()
+            if self._user == fork_info["user"]["name"]:
+                return fork
+
+        if not self.is_forked():
+            if create:
+                return self.fork_create()
+
+            logger.info(
+                f"Fork of {self.repo}"
+                " does not exist and we were asked not to create it.",
+            )
+            return None
+        return self._construct_fork_project()
+
+    def exists(self) -> bool:
+        response = self._call_project_api_raw()
+        return response.ok
+
+    def is_private(self) -> bool:
+        host = urlparse(self.service.instance_url).hostname
+        if host in [
+            "git.centos.org",
+            "git.stg.centos.org",
+            "pagure.io",
+            "src.fedoraproject.org",
+            "src.stg.fedoraproject.org",
+        ]:
+            # private repositories are not allowed on generally used pagure instances
+            return False
+        raise OperationNotSupported(
+            f"is_private is not implemented for {self.service.instance_url}."
+            f"Please open issue in https://github.com/packit/ogr",
+        )
+
+    def is_forked(self) -> bool:
+        f = self._construct_fork_project()
+        return bool(f.exists() and f.parent.exists())
+
+    def get_is_fork_from_api(self) -> bool:
+        return bool(self.get_project_info()["parent"])
+
+    @property
+    def is_fork(self) -> bool:
+        return self._is_fork
+
+    @property
+    def parent(self) -> Optional["PagureProject"]:
+        if self.get_is_fork_from_api():
+            return PagureProject(
+                repo=self.repo,
+                namespace=self.get_project_info()["parent"]["namespace"],
+                service=self.service,
+            )
+        return None
+
+    def get_git_urls(self) -> dict[str, str]:
+        return_value = self._call_project_api("git", "urls")
+        return return_value["urls"]
+
+    def add_user(self, user: str, access_level: AccessLevel) -> None:
+        self.add_user_or_group(user, access_level, "user")
+
+    def remove_user(self, user: str) -> None:
+        self.add_user_or_group(user, None, "user")
+
+    def add_group(self, group: str, access_level: AccessLevel):
+        self.add_user_or_group(group, access_level, "group")
+
+    def remove_group(self, group: str) -> None:
+        self.add_user_or_group(group, None, "group")
+
+    def add_user_or_group(
+        self,
+        user: str,
+        access_level: Optional[AccessLevel],
+        user_type: str,
+    ) -> None:
+        access_dict = {
+            AccessLevel.pull: "ticket",
+            AccessLevel.triage: "ticket",
+            AccessLevel.push: "commit",
+            AccessLevel.admin: "commit",
+            AccessLevel.maintain: "admin",
+            None: "",
+        }
+        response = self._call_project_api_raw(
+            "git",
+            "modifyacls",
+            method="POST",
+            data={
+                "user_type": user_type,
+                "name": user,
+                "acl": access_dict[access_level],
+            },
+        )
+
+        if response.status_code == 401:
+            raise PagureAPIException(
+                "You are not allowed to modify ACL's",
+                response_code=response.status_code,
+            )
+
+    def change_token(self, new_token: str) -> None:
+        self.service.change_token(new_token)
+
+    def get_file_content(self, path: str, ref=None) -> str:
+        ref = ref or self.default_branch
+        result = self._call_project_api_raw(
+            "raw",
+            ref,
+            "f",
+            path,
+            add_api_endpoint_part=False,
+        )
+
+        if not result or result.reason == "NOT FOUND":
+            raise FileNotFoundError(f"File '{path}' on {ref} not found")
+        if result.reason != "OK":
+            raise PagureAPIException(
+                f"File '{path}' on {ref} not found due to {result.reason}",
+            )
+        return result.content.decode()
+
+    def get_sha_from_tag(self, tag_name: str) -> str:
+        tags_dict = self.get_tags_dict()
+        if tag_name not in tags_dict:
+            raise PagureAPIException(f"Tag '{tag_name}' not found.", response_code=404)
+
+        return tags_dict[tag_name].commit_sha
+
+    def commit_comment(
+        self,
+        commit: str,
+        body: str,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> CommitComment:
+        raise OperationNotSupported("Commit comments are not supported on Pagure.")
+
+    def get_commit_comments(self, commit: str) -> list[CommitComment]:
+        raise OperationNotSupported("Commit comments are not supported on Pagure.")
+
+    @if_readonly(return_function=GitProjectReadOnly.set_commit_status)
+    @indirect(PagureCommitFlag.set)
+    def set_commit_status(
+        self,
+        commit: str,
+        state: CommitStatus,
+        target_url: str,
+        description: str,
+        context: str,
+        percent: Optional[int] = None,
+        uid: Optional[str] = None,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        pass
+
+    @indirect(PagureCommitFlag.get)
+    def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+        pass
+
+    def get_tags(self) -> list[GitTag]:
+        response = self._call_project_api("git", "tags", params={"with_commits": True})
+        return [GitTag(name=n, commit_sha=c) for n, c in response["tags"].items()]
+
+    def get_tags_dict(self) -> dict[str, GitTag]:
+        response = self._call_project_api("git", "tags", params={"with_commits": True})
+        return {n: GitTag(name=n, commit_sha=c) for n, c in response["tags"].items()}
+
+    @indirect(PagureRelease.get_list)
+    def get_releases(self) -> list[Release]:
+        pass
+
+    @indirect(PagureRelease.get)
+    def get_release(self, identifier=None, name=None, tag_name=None) -> PagureRelease:
+        pass
+
+    @indirect(PagureRelease.get_latest)
+    def get_latest_release(self) -> Optional[PagureRelease]:
+        pass
+
+    @indirect(PagureRelease.create)
+    def create_release(
+        self,
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> Release:
+        pass
+
+    def get_forks(self) -> list["PagureProject"]:
+        forks_url = self.service.get_api_url("projects")
+        projects_response = self.service.call_api(
+            url=forks_url,
+            params={"fork": True, "pattern": self.repo},
+        )
+        return [
+            PagureProject(
+                repo=fork["name"],
+                namespace=fork["namespace"],
+                service=self.service,
+                username=fork["user"]["name"],
+                is_fork=True,
+            )
+            for fork in projects_response["projects"]
+        ]
+
+    def get_web_url(self) -> str:
+        return f'{self.service.instance_url}/{self.get_project_info()["url_path"]}'
+
+    @property
+    def full_repo_name(self) -> str:
+        fork = f"fork/{self._user}/" if self.is_fork else ""
+        namespace = f"{self.namespace}/" if self.namespace else ""
+        return f"{fork}{namespace}{self.repo}"
+
+    def __get_files(
+        self,
+        path: str,
+        ref: Optional[str] = None,
+        recursive: bool = False,
+    ) -> Iterable[str]:
+        subfolders = ["."]
+
+        while subfolders:
+            path = subfolders.pop()
+            split_path = []
+            if path != ".":
+                split_path = ["f", *path.split("/")]
+            response = self._call_project_api("tree", ref, *split_path)
+
+            for file in response["content"]:
+                if file["type"] == "file":
+                    yield file["path"]
+                elif recursive and file["type"] == "folder":
+                    subfolders.append(file["path"])
+
+    def get_files(
+        self,
+        ref: Optional[str] = None,
+        filter_regex: Optional[str] = None,
+        recursive: bool = False,
+    ) -> list[str]:
+        ref = ref or self.default_branch
+        paths = list(self.__get_files(".", ref, recursive))
+        if filter_regex:
+            paths = filter_paths(paths, filter_regex)
+
+        return paths
+
+    def get_sha_from_branch(self, branch: str) -> Optional[str]:
+        branches = self._call_project_api(
+            "git",
+            "branches",
+            params={"with_commits": True},
+        )["branches"]
+
+        return branches.get(branch)
+
+    def get_contributors(self) -> set[str]:
+        raise OperationNotSupported("Pagure doesn't provide list of contributors")
+
+    def users_with_write_access(self) -> set[str]:
+        users_with_access = self.get_project_info()["access_users"]
+        result = set()
+        for access_level in ["commit", "admin", "owner"]:
+            result.update(users_with_access[access_level])
+
+        return result
+
+

Ancestors

+ +

Class variables

+
+
var servicePagureService
+
+
+
+
+

Methods

+
+
+def add_user_or_group(self, user: str, access_level: Optional[AccessLevel], user_type: str) ‑> None +
+
+
+
+ +Expand source code + +
def add_user_or_group(
+    self,
+    user: str,
+    access_level: Optional[AccessLevel],
+    user_type: str,
+) -> None:
+    access_dict = {
+        AccessLevel.pull: "ticket",
+        AccessLevel.triage: "ticket",
+        AccessLevel.push: "commit",
+        AccessLevel.admin: "commit",
+        AccessLevel.maintain: "admin",
+        None: "",
+    }
+    response = self._call_project_api_raw(
+        "git",
+        "modifyacls",
+        method="POST",
+        data={
+            "user_type": user_type,
+            "name": user,
+            "acl": access_dict[access_level],
+        },
+    )
+
+    if response.status_code == 401:
+        raise PagureAPIException(
+            "You are not allowed to modify ACL's",
+            response_code=response.status_code,
+        )
+
+
+
+def get_is_fork_from_api(self) ‑> bool +
+
+
+
+ +Expand source code + +
def get_is_fork_from_api(self) -> bool:
+    return bool(self.get_project_info()["parent"])
+
+
+
+def get_project_info(self) +
+
+
+
+ +Expand source code + +
def get_project_info(self):
+    return self._call_project_api(method="GET")
+
+
+
+def get_tags_dict(self) ‑> dict[str, GitTag] +
+
+
+
+ +Expand source code + +
def get_tags_dict(self) -> dict[str, GitTag]:
+    response = self._call_project_api("git", "tags", params={"with_commits": True})
+    return {n: GitTag(name=n, commit_sha=c) for n, c in response["tags"].items()}
+
+
+
+

Inherited members

+ +
+
+class PagurePullRequest +(raw_pr, project) +
+
+

Attributes

+
+
project : GitProject
+
Project of the pull request.
+
+
+ +Expand source code + +
class PagurePullRequest(BasePullRequest):
+    _target_project: "ogr_pagure.PagureProject"
+    _source_project: "ogr_pagure.PagureProject" = None
+
+    def __init__(self, raw_pr, project):
+        super().__init__(raw_pr, project)
+        self.__dirty = False
+
+    def __update(self):
+        if self.__dirty:
+            self._raw_pr = self.__call_api()
+            self.__dirty = False
+
+    @property
+    def title(self) -> str:
+        self.__update()
+        return self._raw_pr["title"]
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self.update_info(title=new_title)
+
+    @property
+    def id(self) -> int:
+        return self._raw_pr["id"]
+
+    @property
+    def status(self) -> PRStatus:
+        self.__update()
+        return PRStatus[self._raw_pr["status"].lower()]
+
+    @property
+    def url(self) -> str:
+        return "/".join(
+            [
+                self.target_project.service.instance_url,
+                self._raw_pr["project"]["url_path"],
+                "pull-request",
+                str(self.id),
+            ],
+        )
+
+    @property
+    def description(self) -> str:
+        self.__update()
+        return self._raw_pr["initial_comment"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.update_info(description=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_pr["user"]["name"]
+
+    @property
+    def source_branch(self) -> str:
+        return self._raw_pr["branch_from"]
+
+    @property
+    def target_branch(self) -> str:
+        return self._raw_pr["branch"]
+
+    @property
+    def created(self) -> datetime.datetime:
+        return datetime.datetime.fromtimestamp(int(self._raw_pr["date_created"]))
+
+    @property
+    def diff_url(self) -> str:
+        return f"{self.url}#request_diff"
+
+    @property
+    def commits_url(self) -> str:
+        return f"{self.url}#commit_list"
+
+    @property
+    def patch(self) -> bytes:
+        request_response = self._target_project._call_project_api_raw(
+            "pull-request",
+            f"{self.id}.patch",
+            add_api_endpoint_part=False,
+        )
+        if request_response.status_code != 200:
+            raise PagureAPIException(
+                f"Cannot get patch from {self.url}.patch because {request_response.reason}.",
+                response_code=request_response.status_code,
+            )
+        return request_response.content
+
+    @property
+    def head_commit(self) -> str:
+        return self._raw_pr["commit_stop"]
+
+    @property
+    def source_project(self) -> "ogr_pagure.PagureProject":
+        if self._source_project is None:
+            source = self._raw_pr["repo_from"]
+            source_project_info = {
+                "repo": source["name"],
+                "namespace": source["namespace"],
+            }
+
+            if source["parent"] is not None:
+                source_project_info["is_fork"] = True
+                source_project_info["username"] = source["user"]["name"]
+
+            self._source_project = self._target_project.service.get_project(
+                **source_project_info,
+            )
+
+        return self._source_project
+
+    @property
+    def closed_by(self) -> Optional[str]:
+        closed_by = self._raw_pr["closed_by"]
+        return closed_by["name"] if closed_by else None
+
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+    def __call_api(self, *args, **kwargs) -> dict:
+        return self._target_project._call_project_api(
+            "pull-request",
+            str(self.id),
+            *args,
+            **kwargs,
+        )
+
+    @staticmethod
+    def create(
+        project: "ogr_pagure.PagureProject",
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        data = {
+            "title": title,
+            "branch_to": target_branch,
+            "branch_from": source_branch,
+            "initial_comment": body,
+        }
+
+        caller = project
+        if project.is_fork:
+            data["repo_from"] = project.repo
+            data["repo_from_username"] = project._user
+            data["repo_from_namespace"] = project.namespace
+
+            # running the call from the parent project
+            caller = caller.parent
+        elif fork_username:
+            fork_project = project.service.get_project(
+                username=fork_username,
+                repo=project.repo,
+                namespace=project.namespace,
+                is_fork=True,
+            )
+            data["repo_from_username"] = fork_username
+            data["repo_from"] = fork_project.repo
+            data["repo_from_namespace"] = fork_project.namespace
+
+        response = caller._call_project_api(
+            "pull-request",
+            "new",
+            method="POST",
+            data=data,
+        )
+        return PagurePullRequest(response, caller)
+
+    @staticmethod
+    def get(project: "ogr_pagure.PagureProject", pr_id: int) -> "PullRequest":
+        raw_pr = project._call_project_api("pull-request", str(pr_id))
+        return PagurePullRequest(raw_pr, project)
+
+    @staticmethod
+    def get_files_diff(
+        project: "ogr_pagure.PagureProject",
+        pr_id: int,
+        retries: int = 0,
+        wait_seconds: int = 3,
+    ) -> dict:
+        """
+        Retrieve pull request diff statistics.
+
+        Pagure API tends to return ENOPRSTATS error when a pull request is transitioning
+        from open to other states, so you can use `retries` and `wait_seconds` to try to
+        mitigate that.
+
+
+        Args:
+            project: Pagure project.
+            pr_id: Pull request ID.
+            retries: Number of extra attempts.
+            wait_seconds: Delay between attempts.
+        """
+        attempt = 1
+        while True:
+            try:
+                return project._call_project_api(
+                    "pull-request",
+                    str(pr_id),
+                    "diffstats",
+                    method="GET",
+                )
+            except PagureAPIException as ex:  # noqa PERF203
+                if "No statistics" in ex.pagure_error:
+                    # this may be a race condition, try once more
+                    logger.info(
+                        f"While retrieving PR diffstats Pagure returned ENOPRSTATS.\n{ex}",
+                    )
+                    if attempt <= retries:
+                        attempt += 1
+                        logger.info(
+                            f"Trying again; attempt={attempt} after {wait_seconds} seconds",
+                        )
+                        sleep(wait_seconds)
+                        continue
+                raise ex
+
+    @staticmethod
+    def get_list(
+        project: "ogr_pagure.PagureProject",
+        status: PRStatus = PRStatus.open,
+        assignee=None,
+        author=None,
+    ) -> list["PullRequest"]:
+        payload = {"page": 1, "status": status.name.capitalize()}
+        if assignee is not None:
+            payload["assignee"] = assignee
+        if author is not None:
+            payload["author"] = author
+
+        raw_prs = []
+        while True:
+            page_result = project._call_project_api("pull-requests", params=payload)
+            raw_prs += page_result["requests"]
+            if not page_result["pagination"]["next"]:
+                break
+
+            # mypy don't know that key "page" really contains int...
+            payload["page"] += 1  # type: ignore
+
+        return [PagurePullRequest(pr_dict, project) for pr_dict in raw_prs]
+
+    def update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "PullRequest":
+        try:
+            data = {"title": title if title else self.title}
+
+            if description:
+                data["initial_comment"] = description
+
+            updated_pr = self.__call_api(method="POST", data=data)
+            logger.info("PR updated.")
+
+            self._raw_pr = updated_pr
+            return self
+        except Exception as ex:
+            raise PagureAPIException("there was an error while updating the PR") from ex
+
+    def _get_all_comments(self) -> list[PRComment]:
+        self.__update()
+        raw_comments = self._raw_pr["comments"]
+        return [
+            PagurePRComment(parent=self, raw_comment=comment_dict)
+            for comment_dict in raw_comments
+        ]
+
+    def comment(
+        self,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        payload: dict[str, Any] = {"comment": body}
+        if commit is not None:
+            payload["commit"] = commit
+        if filename is not None:
+            payload["filename"] = filename
+        if row is not None:
+            payload["row"] = row
+
+        self.__call_api("comment", method="POST", data=payload)
+        self.__dirty = True
+        return PagurePRComment(
+            parent=self,
+            body=body,
+            author=self.target_project.service.user.get_username(),
+        )
+
+    def close(self) -> "PullRequest":
+        return_value = self.__call_api("close", method="POST")
+
+        if return_value["message"] != "Pull-request closed!":
+            raise PagureAPIException(return_value["message"])
+
+        self.__dirty = True
+        return self
+
+    def merge(self) -> "PullRequest":
+        return_value = self.__call_api("merge", method="POST")
+
+        if return_value["message"] != "Changes merged!":
+            raise PagureAPIException(return_value["message"])
+
+        self.__dirty = True
+        return self
+
+    def get_statuses(self) -> list[CommitFlag]:
+        self.__update()
+        return self.target_project.get_commit_statuses(self._raw_pr["commit_stop"])
+
+    def set_flag(
+        self,
+        username: str,
+        comment: str,
+        url: str,
+        status: Optional[CommitStatus] = None,
+        percent: Optional[int] = None,
+        uid: Optional[str] = None,
+    ) -> dict:
+        """
+        Set a flag on a pull-request to display results or status of CI tasks.
+
+        See "Flag a pull-request" at https://pagure.io/api/0/#pull_requests-tab
+        for a full description of the parameters.
+
+        Args:
+            username: The name of the application to be presented to users
+                on the pull request page.
+            comment: A short message summarizing the presented results.
+            url: A URL to the result of this flag.
+            status: The status to be displayed for this flag.
+            percent: A percentage of completion compared to the goal.
+            uid: A unique identifier used to identify a flag on the pull-request.
+
+        Returns:
+            Dictionary with the response received from Pagure.
+        """
+        data: dict[str, Union[str, int]] = {
+            "username": username,
+            "comment": comment,
+            "url": url,
+        }
+        if status is not None:
+            data["status"] = status.name
+        if percent is not None:
+            data["percent"] = percent
+        if uid is not None:
+            data["uid"] = uid
+        return self.__call_api("flag", method="POST", data=data)
+
+    def get_comment(self, comment_id: int) -> PRComment:
+        for comment in self._get_all_comments():
+            if comment.id == comment_id:
+                return comment
+
+        raise PagureAPIException(
+            f"No comment with id#{comment_id} in PR#{self.id} found.",
+            response_code=404,
+        )
+
+

Ancestors

+ +

Static methods

+
+
+def get_files_diff(project: ogr_pagure.PagureProject, pr_id: int, retries: int = 0, wait_seconds: int = 3) ‑> dict +
+
+

Retrieve pull request diff statistics.

+

Pagure API tends to return ENOPRSTATS error when a pull request is transitioning +from open to other states, so you can use retries and wait_seconds to try to +mitigate that.

+

Args

+
+
project
+
Pagure project.
+
pr_id
+
Pull request ID.
+
retries
+
Number of extra attempts.
+
wait_seconds
+
Delay between attempts.
+
+
+ +Expand source code + +
@staticmethod
+def get_files_diff(
+    project: "ogr_pagure.PagureProject",
+    pr_id: int,
+    retries: int = 0,
+    wait_seconds: int = 3,
+) -> dict:
+    """
+    Retrieve pull request diff statistics.
+
+    Pagure API tends to return ENOPRSTATS error when a pull request is transitioning
+    from open to other states, so you can use `retries` and `wait_seconds` to try to
+    mitigate that.
+
+
+    Args:
+        project: Pagure project.
+        pr_id: Pull request ID.
+        retries: Number of extra attempts.
+        wait_seconds: Delay between attempts.
+    """
+    attempt = 1
+    while True:
+        try:
+            return project._call_project_api(
+                "pull-request",
+                str(pr_id),
+                "diffstats",
+                method="GET",
+            )
+        except PagureAPIException as ex:  # noqa PERF203
+            if "No statistics" in ex.pagure_error:
+                # this may be a race condition, try once more
+                logger.info(
+                    f"While retrieving PR diffstats Pagure returned ENOPRSTATS.\n{ex}",
+                )
+                if attempt <= retries:
+                    attempt += 1
+                    logger.info(
+                        f"Trying again; attempt={attempt} after {wait_seconds} seconds",
+                    )
+                    sleep(wait_seconds)
+                    continue
+            raise ex
+
+
+
+

Methods

+
+
+def set_flag(self, username: str, comment: str, url: str, status: Optional[CommitStatus] = None, percent: Optional[int] = None, uid: Optional[str] = None) ‑> dict +
+
+

Set a flag on a pull-request to display results or status of CI tasks.

+

See "Flag a pull-request" at https://pagure.io/api/0/#pull_requests-tab +for a full description of the parameters.

+

Args

+
+
username
+
The name of the application to be presented to users +on the pull request page.
+
comment
+
A short message summarizing the presented results.
+
url
+
A URL to the result of this flag.
+
status
+
The status to be displayed for this flag.
+
percent
+
A percentage of completion compared to the goal.
+
uid
+
A unique identifier used to identify a flag on the pull-request.
+
+

Returns

+

Dictionary with the response received from Pagure.

+
+ +Expand source code + +
def set_flag(
+    self,
+    username: str,
+    comment: str,
+    url: str,
+    status: Optional[CommitStatus] = None,
+    percent: Optional[int] = None,
+    uid: Optional[str] = None,
+) -> dict:
+    """
+    Set a flag on a pull-request to display results or status of CI tasks.
+
+    See "Flag a pull-request" at https://pagure.io/api/0/#pull_requests-tab
+    for a full description of the parameters.
+
+    Args:
+        username: The name of the application to be presented to users
+            on the pull request page.
+        comment: A short message summarizing the presented results.
+        url: A URL to the result of this flag.
+        status: The status to be displayed for this flag.
+        percent: A percentage of completion compared to the goal.
+        uid: A unique identifier used to identify a flag on the pull-request.
+
+    Returns:
+        Dictionary with the response received from Pagure.
+    """
+    data: dict[str, Union[str, int]] = {
+        "username": username,
+        "comment": comment,
+        "url": url,
+    }
+    if status is not None:
+        data["status"] = status.name
+    if percent is not None:
+        data["percent"] = percent
+    if uid is not None:
+        data["uid"] = uid
+    return self.__call_api("flag", method="POST", data=data)
+
+
+
+

Inherited members

+ +
+
+class PagureRelease +(raw_release: Any, project: GitProject) +
+
+

Object that represents release.

+

Attributes

+
+
project : GitProject
+
Project on which the release is created.
+
+
+ +Expand source code + +
class PagureRelease(Release):
+    _raw_release: GitTag
+    project: "ogr_pagure.PagureProject"
+
+    @property
+    def title(self):
+        return self.git_tag.name
+
+    @property
+    def body(self):
+        return ""
+
+    @property
+    def git_tag(self) -> GitTag:
+        return self._raw_release
+
+    @property
+    def tag_name(self) -> str:
+        return self._raw_release.name
+
+    @property
+    def url(self) -> Optional[str]:
+        return ""
+
+    @property
+    def created_at(self) -> datetime.datetime:
+        return None
+
+    @property
+    def tarball_url(self) -> str:
+        return ""
+
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+    @staticmethod
+    def get(
+        project: "ogr_pagure.PagureProject",
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> "Release":
+        raise OperationNotSupported()
+
+    @staticmethod
+    def get_latest(project: "ogr_pagure.PagureProject") -> Optional["Release"]:
+        raise OperationNotSupported("Pagure API does not provide timestamps")
+
+    @staticmethod
+    def get_list(project: "ogr_pagure.PagureProject") -> list["Release"]:
+        # git tag for Pagure is shown as Release in Pagure UI
+        git_tags = project.get_tags()
+        return [PagureRelease(git_tag, project) for git_tag in git_tags]
+
+    @staticmethod
+    def create(
+        project: "ogr_pagure.PagureProject",
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> "Release":
+        payload = {
+            "tagname": tag,
+            "commit_hash": ref,
+        }
+        if message:
+            payload["message"] = message
+
+        response = project._call_project_api("git", "tags", data=payload, method="POST")
+        if not response["tag_created"]:
+            raise PagureAPIException("Release has not been created")
+
+        return PagureRelease(GitTag(tag, ref), project)
+
+    def edit_release(self, name: str, message: str) -> None:
+        raise OperationNotSupported("edit_release not supported on Pagure")
+
+

Ancestors

+ +

Class variables

+
+
var projectPagureProject
+
+
+
+
+

Inherited members

+ +
+
+class PagureService +(token: Optional[str] = None, instance_url: str = 'https://src.fedoraproject.org', read_only: bool = False, insecure: bool = False, max_retries: Union[int, urllib3.util.retry.Retry] = 5, **kwargs) +
+
+

Attributes

+
+
instance_url : str
+
URL of the git forge instance.
+
+
+ +Expand source code + +
@use_for_service("pagure")
+@use_for_service("src.fedoraproject.org")
+@use_for_service("src.stg.fedoraproject.org")
+@use_for_service("pkgs.fedoraproject.org")
+@use_for_service("pkgs.stg.fedoraproject.org")
+@use_for_service("git.centos.org")
+@use_for_service("git.stg.centos.org")
+class PagureService(BaseGitService):
+    def __init__(
+        self,
+        token: Optional[str] = None,
+        instance_url: str = "https://src.fedoraproject.org",
+        read_only: bool = False,
+        insecure: bool = False,
+        max_retries: Union[int, urllib3.util.Retry] = 5,
+        **kwargs,
+    ) -> None:
+        super().__init__()
+        self.instance_url = instance_url
+        self._token = token
+        self.read_only = read_only
+
+        self.session = requests.session()
+
+        adapter = requests.adapters.HTTPAdapter(max_retries=max_retries)
+
+        self.insecure = insecure
+        if self.insecure:
+            self.session.mount("http://", adapter)
+        else:
+            self.session.mount("https://", adapter)
+
+        self.header = {"Authorization": "token " + self._token} if self._token else {}
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    def __str__(self) -> str:
+        token_str = (
+            f", token='{self._token[:1]}***{self._token[-1:]}'" if self._token else ""
+        )
+        insecure_str = ", insecure=True" if self.insecure else ""
+        readonly_str = ", read_only=True" if self.read_only else ""
+
+        return (
+            f"PagureService(instance_url='{self.instance_url}'"
+            f"{token_str}"
+            f"{readonly_str}"
+            f"{insecure_str})"
+        )
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, PagureService):
+            return False
+
+        return (
+            self._token == o._token  # type: ignore
+            and self.read_only == o.read_only  # type: ignore
+            and self.instance_url == o.instance_url  # type: ignore
+            and self.insecure == o.insecure  # type: ignore
+            and self.header == o.header  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(self, **kwargs) -> "PagureProject":
+        if "username" in kwargs:
+            return PagureProject(service=self, **kwargs)
+
+        return PagureProject(
+            service=self,
+            username=self.user.get_username(),
+            **kwargs,
+        )
+
+    def get_project_from_url(self, url: str) -> "PagureProject":
+        repo_url = parse_git_repo(potential_url=url)
+        if not repo_url:
+            raise OgrException(f"Cannot parse project url: '{url}'")
+
+        if not repo_url.is_fork:
+            repo_url.username = None
+
+        return self.get_project(
+            repo=repo_url.repo,
+            namespace=repo_url.namespace,
+            is_fork=repo_url.is_fork,
+            username=repo_url.username,
+        )
+
+    @property
+    def user(self) -> "PagureUser":
+        return PagureUser(service=self)
+
+    def call_api(
+        self,
+        url: str,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data=None,
+    ) -> dict:
+        """
+        Call API endpoint.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            Dictionary representing response.
+
+        Raises:
+            PagureAPIException, if error occurs.
+        """
+        response = self.call_api_raw(url=url, method=method, params=params, data=data)
+
+        if response.status_code == 404:
+            error_msg = (
+                response.json_content["error"]
+                if response.json_content and "error" in response.json_content
+                else None
+            )
+            raise PagureAPIException(
+                f"Page '{url}' not found when calling Pagure API.",
+                pagure_error=error_msg,
+                response_code=response.status_code,
+            )
+
+        if not response.json_content:
+            logger.debug(response.content)
+            raise PagureAPIException(
+                "Error while decoding JSON: {0}",
+                response_code=response.status_code,
+            )
+
+        if not response.ok:
+            logger.error(response.json_content)
+            if "error" in response.json_content:
+                error_msg = response.json_content["error"]
+                error_msg_ext = response.json_content.get("errors", "")
+                msg = f"Pagure API returned an error when calling '{url}': {error_msg}"
+                if error_msg_ext:
+                    msg += f" - {error_msg_ext}"
+                raise PagureAPIException(
+                    msg,
+                    pagure_error=error_msg,
+                    pagure_response=response.json_content,
+                    response_code=response.status_code,
+                )
+            raise PagureAPIException(
+                f"Problem with Pagure API when calling '{url}'",
+                response_code=response.status_code,
+            )
+
+        return response.json_content
+
+    def call_api_raw(
+        self,
+        url: str,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data=None,
+    ):
+        """
+        Call API endpoint and returns raw response.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            `RequestResponse` object that represents the response from the API
+            endpoint.
+        """
+
+        method = method or "GET"
+        try:
+            response = self.get_raw_request(
+                method=method,
+                url=url,
+                params=params,
+                data=data,
+            )
+
+        except requests.exceptions.ConnectionError as er:
+            logger.error(er)
+            raise OgrNetworkError(f"Cannot connect to url: '{url}'.") from er
+
+        if response.status_code >= 500:
+            raise GitForgeInternalError(
+                f"Pagure API returned {response.status_code} status for `{url}`"
+                f" with reason: `{response.reason}`",
+            )
+
+        return response
+
+    def get_raw_request(
+        self,
+        url,
+        method="GET",
+        params=None,
+        data=None,
+        header=None,
+    ) -> RequestResponse:
+        """
+        Call API endpoint and wrap the response in `RequestResponse` type.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+
+                Defaults to `"GET"`.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+            header: Header of the HTTP request.
+
+        Returns:
+            `RequestResponse` object representing the response.
+
+        Raises:
+            ValueError, if JSON cannot be retrieved.
+        """
+
+        response = self.session.request(
+            method=method,
+            url=url,
+            params=params,
+            headers=header or self.header,
+            data=data,
+            verify=not self.insecure,
+        )
+
+        json_output = None
+        try:
+            json_output = response.json()
+        except ValueError:
+            logger.debug(response.text)
+
+        return RequestResponse(
+            status_code=response.status_code,
+            ok=response.ok,
+            content=response.content,
+            json=json_output,
+            reason=response.reason,
+        )
+
+    @property
+    def api_url(self):
+        """URL to the Pagure API."""
+        return f"{self.instance_url}/api/0/"
+
+    def get_api_url(self, *args, add_api_endpoint_part: bool = True) -> str:
+        """
+        Get a URL from its parts.
+
+        Args:
+            *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+            add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+                Defaults to `True`.
+
+        Returns:
+            String
+        """
+        args_list: list[str] = []
+
+        args_list += filter(lambda x: x is not None, args)
+
+        if add_api_endpoint_part:
+            return self.api_url + "/".join(args_list)
+        return f"{self.instance_url}/" + "/".join(args_list)
+
+    def get_api_version(self) -> str:
+        """
+        Returns:
+            Version of the Pagure API.
+        """
+        request_url = self.get_api_url("version")
+        return_value = self.call_api(request_url)
+        return return_value["version"]
+
+    def get_error_codes(self):
+        """
+        Returns:
+            Dictionary with all error codes.
+        """
+        request_url = self.get_api_url("error_codes")
+        return self.call_api(request_url)
+
+    def change_token(self, token: str):
+        self._token = token
+        self.header = {"Authorization": "token " + self._token}
+
+    def __handle_project_create_fail(
+        self,
+        exception: PagureAPIException,
+        namespace: str,
+    ) -> None:
+        if (
+            exception.pagure_response
+            and exception.pagure_response["errors"]["namespace"][0]
+            == "Not a valid choice"
+        ):
+            request_url = self.get_api_url("group", namespace)
+
+            try:
+                self.call_api(request_url, data={"projects": False})
+            except PagureAPIException as ex:
+                raise OgrException(f"Namespace doesn't exist ({namespace}).") from ex
+
+            raise OgrException(
+                "Cannot create project in given namespace (permissions).",
+            )
+
+        raise exception
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> PagureProject:
+        request_url = self.get_api_url("new")
+
+        parameters = {"name": repo, "description": description, "wait": True}
+        if not description:
+            parameters["description"] = repo
+        if namespace:
+            parameters["namespace"] = namespace
+
+        try:
+            self.call_api(request_url, "POST", data=parameters)
+        except PagureAPIException as ex:
+            self.__handle_project_create_fail(ex, namespace)
+        return PagureProject(repo=repo, namespace=namespace, service=self)
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        raise OperationNotSupported
+
+

Ancestors

+ +

Instance variables

+
+
var api_url
+
+

URL to the Pagure API.

+
+ +Expand source code + +
@property
+def api_url(self):
+    """URL to the Pagure API."""
+    return f"{self.instance_url}/api/0/"
+
+
+
+

Methods

+
+
+def call_api(self, url: str, method: Optional[str] = None, params: Optional[dict] = None, data=None) ‑> dict +
+
+

Call API endpoint.

+

Args

+
+
url
+
URL to be called.
+
method
+
Method of the HTTP request, e.g. "GET", "POST", etc.
+
params
+
HTTP(S) query parameters in form of a dictionary.
+
data
+
Data to be sent in form of a dictionary.
+
+

Returns

+

Dictionary representing response.

+

Raises

+

PagureAPIException, if error occurs.

+
+ +Expand source code + +
def call_api(
+    self,
+    url: str,
+    method: Optional[str] = None,
+    params: Optional[dict] = None,
+    data=None,
+) -> dict:
+    """
+    Call API endpoint.
+
+    Args:
+        url: URL to be called.
+        method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+        params: HTTP(S) query parameters in form of a dictionary.
+        data: Data to be sent in form of a dictionary.
+
+    Returns:
+        Dictionary representing response.
+
+    Raises:
+        PagureAPIException, if error occurs.
+    """
+    response = self.call_api_raw(url=url, method=method, params=params, data=data)
+
+    if response.status_code == 404:
+        error_msg = (
+            response.json_content["error"]
+            if response.json_content and "error" in response.json_content
+            else None
+        )
+        raise PagureAPIException(
+            f"Page '{url}' not found when calling Pagure API.",
+            pagure_error=error_msg,
+            response_code=response.status_code,
+        )
+
+    if not response.json_content:
+        logger.debug(response.content)
+        raise PagureAPIException(
+            "Error while decoding JSON: {0}",
+            response_code=response.status_code,
+        )
+
+    if not response.ok:
+        logger.error(response.json_content)
+        if "error" in response.json_content:
+            error_msg = response.json_content["error"]
+            error_msg_ext = response.json_content.get("errors", "")
+            msg = f"Pagure API returned an error when calling '{url}': {error_msg}"
+            if error_msg_ext:
+                msg += f" - {error_msg_ext}"
+            raise PagureAPIException(
+                msg,
+                pagure_error=error_msg,
+                pagure_response=response.json_content,
+                response_code=response.status_code,
+            )
+        raise PagureAPIException(
+            f"Problem with Pagure API when calling '{url}'",
+            response_code=response.status_code,
+        )
+
+    return response.json_content
+
+
+
+def call_api_raw(self, url: str, method: Optional[str] = None, params: Optional[dict] = None, data=None) +
+
+

Call API endpoint and returns raw response.

+

Args

+
+
url
+
URL to be called.
+
method
+
Method of the HTTP request, e.g. "GET", "POST", etc.
+
params
+
HTTP(S) query parameters in form of a dictionary.
+
data
+
Data to be sent in form of a dictionary.
+
+

Returns

+

RequestResponse object that represents the response from the API +endpoint.

+
+ +Expand source code + +
def call_api_raw(
+    self,
+    url: str,
+    method: Optional[str] = None,
+    params: Optional[dict] = None,
+    data=None,
+):
+    """
+    Call API endpoint and returns raw response.
+
+    Args:
+        url: URL to be called.
+        method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+        params: HTTP(S) query parameters in form of a dictionary.
+        data: Data to be sent in form of a dictionary.
+
+    Returns:
+        `RequestResponse` object that represents the response from the API
+        endpoint.
+    """
+
+    method = method or "GET"
+    try:
+        response = self.get_raw_request(
+            method=method,
+            url=url,
+            params=params,
+            data=data,
+        )
+
+    except requests.exceptions.ConnectionError as er:
+        logger.error(er)
+        raise OgrNetworkError(f"Cannot connect to url: '{url}'.") from er
+
+    if response.status_code >= 500:
+        raise GitForgeInternalError(
+            f"Pagure API returned {response.status_code} status for `{url}`"
+            f" with reason: `{response.reason}`",
+        )
+
+    return response
+
+
+
+def get_api_url(self, *args, add_api_endpoint_part: bool = True) ‑> str +
+
+

Get a URL from its parts.

+

Args

+
+
*args
+
String parts of the URL, e.g. "a", "b" will call project/a/b
+
add_api_endpoint_part
+
+

Add part with API endpoint (/api/0/).

+

Defaults to True.

+
+
+

Returns

+

String

+
+ +Expand source code + +
def get_api_url(self, *args, add_api_endpoint_part: bool = True) -> str:
+    """
+    Get a URL from its parts.
+
+    Args:
+        *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+        add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+            Defaults to `True`.
+
+    Returns:
+        String
+    """
+    args_list: list[str] = []
+
+    args_list += filter(lambda x: x is not None, args)
+
+    if add_api_endpoint_part:
+        return self.api_url + "/".join(args_list)
+    return f"{self.instance_url}/" + "/".join(args_list)
+
+
+
+def get_api_version(self) ‑> str +
+
+

Returns

+

Version of the Pagure API.

+
+ +Expand source code + +
def get_api_version(self) -> str:
+    """
+    Returns:
+        Version of the Pagure API.
+    """
+    request_url = self.get_api_url("version")
+    return_value = self.call_api(request_url)
+    return return_value["version"]
+
+
+
+def get_error_codes(self) +
+
+

Returns

+

Dictionary with all error codes.

+
+ +Expand source code + +
def get_error_codes(self):
+    """
+    Returns:
+        Dictionary with all error codes.
+    """
+    request_url = self.get_api_url("error_codes")
+    return self.call_api(request_url)
+
+
+
+def get_raw_request(self, url, method='GET', params=None, data=None, header=None) ‑> RequestResponse +
+
+

Call API endpoint and wrap the response in RequestResponse type.

+

Args

+
+
url
+
URL to be called.
+
method
+
+

Method of the HTTP request, e.g. "GET", "POST", etc.

+

Defaults to "GET".

+
+
params
+
HTTP(S) query parameters in form of a dictionary.
+
data
+
Data to be sent in form of a dictionary.
+
header
+
Header of the HTTP request.
+
+

Returns

+

RequestResponse object representing the response.

+

Raises

+

ValueError, if JSON cannot be retrieved.

+
+ +Expand source code + +
def get_raw_request(
+    self,
+    url,
+    method="GET",
+    params=None,
+    data=None,
+    header=None,
+) -> RequestResponse:
+    """
+    Call API endpoint and wrap the response in `RequestResponse` type.
+
+    Args:
+        url: URL to be called.
+        method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+
+            Defaults to `"GET"`.
+        params: HTTP(S) query parameters in form of a dictionary.
+        data: Data to be sent in form of a dictionary.
+        header: Header of the HTTP request.
+
+    Returns:
+        `RequestResponse` object representing the response.
+
+    Raises:
+        ValueError, if JSON cannot be retrieved.
+    """
+
+    response = self.session.request(
+        method=method,
+        url=url,
+        params=params,
+        headers=header or self.header,
+        data=data,
+        verify=not self.insecure,
+    )
+
+    json_output = None
+    try:
+        json_output = response.json()
+    except ValueError:
+        logger.debug(response.text)
+
+    return RequestResponse(
+        status_code=response.status_code,
+        ok=response.ok,
+        content=response.content,
+        json=json_output,
+        reason=response.reason,
+    )
+
+
+
+

Inherited members

+ +
+
+class PagureUser +(service: ogr_pagure.PagureService) +
+
+

Represents currently authenticated user through service.

+
+ +Expand source code + +
class PagureUser(BaseGitUser):
+    service: "ogr_pagure.PagureService"
+
+    def __init__(self, service: "ogr_pagure.PagureService") -> None:
+        super().__init__(service=service)
+
+    def __str__(self) -> str:
+        return f'PagureUser(username="{self.get_username()}")'
+
+    def get_username(self) -> str:
+        request_url = self.service.get_api_url("-", "whoami")
+
+        return_value = self.service.call_api(url=request_url, method="POST", data={})
+        return return_value["username"]
+
+    def get_projects(self) -> list["PagureProject"]:
+        user_url = self.service.get_api_url("user", self.get_username())
+        raw_projects = self.service.call_api(user_url)["repos"]
+
+        return [
+            PagureProject(
+                repo=project["name"],
+                namespace=project["namespace"],
+                service=self.service,
+            )
+            for project in raw_projects
+        ]
+
+    def get_forks(self) -> list["PagureProject"]:
+        user_url = self.service.get_api_url("user", self.get_username())
+        raw_forks = self.service.call_api(user_url)["forks"]
+
+        return [
+            PagureProject(
+                repo=fork["name"],
+                namespace=fork["namespace"],
+                service=self.service,
+                is_fork=True,
+            )
+            for fork in raw_forks
+        ]
+
+    def get_email(self) -> str:
+        # Not supported by Pagure
+        raise OperationNotSupported(
+            "Pagure does not support retrieving of user's email address",
+        )
+
+

Ancestors

+ +

Class variables

+
+
var servicePagureService
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/pagure/issue.html b/docs/services/pagure/issue.html new file mode 100644 index 00000000..301c392a --- /dev/null +++ b/docs/services/pagure/issue.html @@ -0,0 +1,617 @@ + + + + + + +ogr.services.pagure.issue API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.pagure.issue

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from typing import Any, Optional, Union, cast
+
+from ogr.abstract import Issue, IssueComment, IssueStatus
+from ogr.exceptions import (
+    IssueTrackerDisabled,
+    OperationNotSupported,
+    PagureAPIException,
+)
+from ogr.services import pagure as ogr_pagure
+from ogr.services.base import BaseIssue
+from ogr.services.pagure.comments import PagureIssueComment
+
+
+class PagureIssue(BaseIssue):
+    project: "ogr_pagure.PagureProject"
+
+    def __init__(self, raw_issue, project):
+        super().__init__(raw_issue, project)
+        self.__dirty = False
+
+    def __update(self):
+        if self.__dirty:
+            self._raw_issue = self.project._call_project_api("issue", str(self.id))
+            self.__dirty = False
+
+    @property
+    def title(self) -> str:
+        self.__update()
+        return self._raw_issue["title"]
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self.__update_info(title=new_title)
+
+    @property
+    def private(self) -> bool:
+        self.__update()
+        return self._raw_issue["private"]
+
+    @property
+    def id(self) -> int:
+        return self._raw_issue["id"]
+
+    @property
+    def status(self) -> IssueStatus:
+        self.__update()
+        return IssueStatus[self._raw_issue["status"].lower()]
+
+    @property
+    def url(self) -> str:
+        return self.project._get_project_url(
+            "issue",
+            str(self.id),
+            add_api_endpoint_part=False,
+        )
+
+    @property
+    def assignee(self) -> str:
+        self.__update()
+        try:
+            return self._raw_issue["assignee"]["name"]
+        except Exception:
+            return None
+
+    @property
+    def description(self) -> str:
+        self.__update()
+        return self._raw_issue["content"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.__update_info(description=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_issue["user"]["name"]
+
+    @property
+    def created(self) -> datetime.datetime:
+        return datetime.datetime.fromtimestamp(int(self._raw_issue["date_created"]))
+
+    @property
+    def labels(self) -> list[str]:
+        return self._raw_issue["tags"]
+
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+    def __update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        assignee: Optional[str] = None,
+    ) -> None:
+        try:
+            data = {
+                "title": title if title is not None else self.title,
+                "issue_content": description
+                if description is not None
+                else self.description,
+            }
+
+            updated_issue = self.project._call_project_api(
+                "issue",
+                str(self.id),
+                method="POST",
+                data=data,
+            )
+            self._raw_issue = updated_issue["issue"]
+        except Exception as ex:
+            raise PagureAPIException(
+                "there was an error while updating the issue",
+            ) from ex
+
+    @staticmethod
+    def create(
+        project: "ogr_pagure.PagureProject",
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        payload = {"title": title, "issue_content": body}
+        if labels is not None:
+            payload["tag"] = ",".join(labels)
+        if private:
+            payload["private"] = "true"
+        if assignees and len(assignees) > 1:
+            raise OperationNotSupported("Pagure does not support multiple assignees")
+
+        if assignees:
+            payload["assignee"] = assignees[0]
+
+        new_issue = project._call_project_api("new_issue", data=payload, method="POST")[
+            "issue"
+        ]
+        return PagureIssue(new_issue, project)
+
+    @staticmethod
+    def get(project: "ogr_pagure.PagureProject", issue_id: int) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        raw_issue = project._call_project_api("issue", str(issue_id))
+        return PagureIssue(raw_issue, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_pagure.PagureProject",
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        payload: dict[str, Union[str, list[str], int]] = {
+            "status": status.name.capitalize(),
+            "page": 1,
+            "per_page": 100,
+        }
+        if author:
+            payload["author"] = author
+        if assignee:
+            payload["assignee"] = assignee
+        if labels:
+            payload["tags"] = labels
+
+        raw_issues: list[Any] = []
+
+        while True:
+            issues_info = project._call_project_api("issues", params=payload)
+            raw_issues += issues_info["issues"]
+            if not issues_info["pagination"]["next"]:
+                break
+            payload["page"] = cast(int, payload["page"]) + 1
+
+        return [PagureIssue(issue_dict, project) for issue_dict in raw_issues]
+
+    def _get_all_comments(self) -> list[IssueComment]:
+        self.__update()
+        raw_comments = self._raw_issue["comments"]
+        return [
+            PagureIssueComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in raw_comments
+        ]
+
+    def comment(self, body: str) -> IssueComment:
+        payload = {"comment": body}
+        self.project._call_project_api(
+            "issue",
+            str(self.id),
+            "comment",
+            data=payload,
+            method="POST",
+        )
+        self.__dirty = True
+        return PagureIssueComment(parent=self, body=body, author=self.project._user)
+
+    def close(self) -> "PagureIssue":
+        payload = {"status": "Closed"}
+        self.project._call_project_api(
+            "issue",
+            str(self.id),
+            "status",
+            data=payload,
+            method="POST",
+        )
+        self.__dirty = True
+        return self
+
+    def add_assignee(self, *assignees: str) -> None:
+        if len(assignees) > 1:
+            raise OperationNotSupported("Pagure does not support multiple assignees")
+        payload = {"assignee": assignees[0]}
+        self.project._call_project_api(
+            "issue",
+            str(self.id),
+            "assign",
+            data=payload,
+            method="POST",
+        )
+
+    def get_comment(self, comment_id: int) -> IssueComment:
+        return PagureIssueComment(
+            self.project._call_project_api(
+                "issue",
+                str(self.id),
+                "comment",
+                str(comment_id),
+                method="GET",
+            ),
+        )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class PagureIssue +(raw_issue, project) +
+
+

Attributes

+
+
project : GitProject
+
Project of the issue.
+
+
+ +Expand source code + +
class PagureIssue(BaseIssue):
+    project: "ogr_pagure.PagureProject"
+
+    def __init__(self, raw_issue, project):
+        super().__init__(raw_issue, project)
+        self.__dirty = False
+
+    def __update(self):
+        if self.__dirty:
+            self._raw_issue = self.project._call_project_api("issue", str(self.id))
+            self.__dirty = False
+
+    @property
+    def title(self) -> str:
+        self.__update()
+        return self._raw_issue["title"]
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self.__update_info(title=new_title)
+
+    @property
+    def private(self) -> bool:
+        self.__update()
+        return self._raw_issue["private"]
+
+    @property
+    def id(self) -> int:
+        return self._raw_issue["id"]
+
+    @property
+    def status(self) -> IssueStatus:
+        self.__update()
+        return IssueStatus[self._raw_issue["status"].lower()]
+
+    @property
+    def url(self) -> str:
+        return self.project._get_project_url(
+            "issue",
+            str(self.id),
+            add_api_endpoint_part=False,
+        )
+
+    @property
+    def assignee(self) -> str:
+        self.__update()
+        try:
+            return self._raw_issue["assignee"]["name"]
+        except Exception:
+            return None
+
+    @property
+    def description(self) -> str:
+        self.__update()
+        return self._raw_issue["content"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.__update_info(description=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_issue["user"]["name"]
+
+    @property
+    def created(self) -> datetime.datetime:
+        return datetime.datetime.fromtimestamp(int(self._raw_issue["date_created"]))
+
+    @property
+    def labels(self) -> list[str]:
+        return self._raw_issue["tags"]
+
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+    def __update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        assignee: Optional[str] = None,
+    ) -> None:
+        try:
+            data = {
+                "title": title if title is not None else self.title,
+                "issue_content": description
+                if description is not None
+                else self.description,
+            }
+
+            updated_issue = self.project._call_project_api(
+                "issue",
+                str(self.id),
+                method="POST",
+                data=data,
+            )
+            self._raw_issue = updated_issue["issue"]
+        except Exception as ex:
+            raise PagureAPIException(
+                "there was an error while updating the issue",
+            ) from ex
+
+    @staticmethod
+    def create(
+        project: "ogr_pagure.PagureProject",
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        payload = {"title": title, "issue_content": body}
+        if labels is not None:
+            payload["tag"] = ",".join(labels)
+        if private:
+            payload["private"] = "true"
+        if assignees and len(assignees) > 1:
+            raise OperationNotSupported("Pagure does not support multiple assignees")
+
+        if assignees:
+            payload["assignee"] = assignees[0]
+
+        new_issue = project._call_project_api("new_issue", data=payload, method="POST")[
+            "issue"
+        ]
+        return PagureIssue(new_issue, project)
+
+    @staticmethod
+    def get(project: "ogr_pagure.PagureProject", issue_id: int) -> "Issue":
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        raw_issue = project._call_project_api("issue", str(issue_id))
+        return PagureIssue(raw_issue, project)
+
+    @staticmethod
+    def get_list(
+        project: "ogr_pagure.PagureProject",
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list["Issue"]:
+        if not project.has_issues:
+            raise IssueTrackerDisabled()
+
+        payload: dict[str, Union[str, list[str], int]] = {
+            "status": status.name.capitalize(),
+            "page": 1,
+            "per_page": 100,
+        }
+        if author:
+            payload["author"] = author
+        if assignee:
+            payload["assignee"] = assignee
+        if labels:
+            payload["tags"] = labels
+
+        raw_issues: list[Any] = []
+
+        while True:
+            issues_info = project._call_project_api("issues", params=payload)
+            raw_issues += issues_info["issues"]
+            if not issues_info["pagination"]["next"]:
+                break
+            payload["page"] = cast(int, payload["page"]) + 1
+
+        return [PagureIssue(issue_dict, project) for issue_dict in raw_issues]
+
+    def _get_all_comments(self) -> list[IssueComment]:
+        self.__update()
+        raw_comments = self._raw_issue["comments"]
+        return [
+            PagureIssueComment(parent=self, raw_comment=raw_comment)
+            for raw_comment in raw_comments
+        ]
+
+    def comment(self, body: str) -> IssueComment:
+        payload = {"comment": body}
+        self.project._call_project_api(
+            "issue",
+            str(self.id),
+            "comment",
+            data=payload,
+            method="POST",
+        )
+        self.__dirty = True
+        return PagureIssueComment(parent=self, body=body, author=self.project._user)
+
+    def close(self) -> "PagureIssue":
+        payload = {"status": "Closed"}
+        self.project._call_project_api(
+            "issue",
+            str(self.id),
+            "status",
+            data=payload,
+            method="POST",
+        )
+        self.__dirty = True
+        return self
+
+    def add_assignee(self, *assignees: str) -> None:
+        if len(assignees) > 1:
+            raise OperationNotSupported("Pagure does not support multiple assignees")
+        payload = {"assignee": assignees[0]}
+        self.project._call_project_api(
+            "issue",
+            str(self.id),
+            "assign",
+            data=payload,
+            method="POST",
+        )
+
+    def get_comment(self, comment_id: int) -> IssueComment:
+        return PagureIssueComment(
+            self.project._call_project_api(
+                "issue",
+                str(self.id),
+                "comment",
+                str(comment_id),
+                method="GET",
+            ),
+        )
+
+

Ancestors

+ +

Class variables

+
+
var projectPagureProject
+
+
+
+
+

Instance variables

+
+
var assignee : str
+
+
+
+ +Expand source code + +
@property
+def assignee(self) -> str:
+    self.__update()
+    try:
+        return self._raw_issue["assignee"]["name"]
+    except Exception:
+        return None
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/pagure/project.html b/docs/services/pagure/project.html new file mode 100644 index 00000000..c5bb2665 --- /dev/null +++ b/docs/services/pagure/project.html @@ -0,0 +1,1422 @@ + + + + + + +ogr.services.pagure.project API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.pagure.project

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import logging
+from collections.abc import Iterable
+from typing import Optional
+from urllib.parse import urlparse
+
+from ogr.abstract import (
+    AccessLevel,
+    CommitComment,
+    CommitFlag,
+    CommitStatus,
+    GitTag,
+    Issue,
+    IssueStatus,
+    PRStatus,
+    PullRequest,
+    Release,
+)
+from ogr.exceptions import (
+    OgrException,
+    OperationNotSupported,
+    PagureAPIException,
+)
+from ogr.read_only import GitProjectReadOnly, if_readonly
+from ogr.services import pagure as ogr_pagure
+from ogr.services.base import BaseGitProject
+from ogr.services.pagure.flag import PagureCommitFlag
+from ogr.services.pagure.issue import PagureIssue
+from ogr.services.pagure.pull_request import PagurePullRequest
+from ogr.services.pagure.release import PagureRelease
+from ogr.utils import RequestResponse, filter_paths, indirect
+
+logger = logging.getLogger(__name__)
+
+
+class PagureProject(BaseGitProject):
+    service: "ogr_pagure.PagureService"
+
+    def __init__(
+        self,
+        repo: str,
+        namespace: Optional[str],
+        service: "ogr_pagure.PagureService",
+        username: Optional[str] = None,
+        is_fork: bool = False,
+    ) -> None:
+        super().__init__(repo, service, namespace)
+        self.read_only = service.read_only
+
+        self._is_fork = is_fork
+        self._username = username
+
+        self.repo = repo
+        self.namespace = namespace
+
+    def __str__(self) -> str:
+        fork_info = ""
+        if self._is_fork:
+            fork_info = f', username="{self._username}", is_fork={self._is_fork}'
+        return f'PagureProject(namespace="{self.namespace}", repo="{self.repo}"{fork_info})'
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, PagureProject):
+            return False
+
+        return (
+            self.repo == o.repo
+            and self.namespace == o.namespace
+            and self.service == o.service
+            and self._username == o._username
+            and self._is_fork == o._is_fork
+            and self.read_only == o.read_only
+        )
+
+    @property
+    def _user(self) -> str:
+        if not self._username:
+            self._username = self.service.user.get_username()
+        return self._username
+
+    def _call_project_api(
+        self,
+        *args,
+        add_fork_part: bool = True,
+        add_api_endpoint_part: bool = True,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data: Optional[dict] = None,
+    ) -> dict:
+        """
+        Call project API endpoint.
+
+        Args:
+            *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+            add_fork_part: If the project is a fork, use `fork/username` prefix.
+
+                Defaults to `True`.
+            add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+                Defaults to `True`.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            Dictionary representing response.
+        """
+        request_url = self._get_project_url(
+            *args,
+            add_api_endpoint_part=add_api_endpoint_part,
+            add_fork_part=add_fork_part,
+        )
+
+        return self.service.call_api(
+            url=request_url,
+            method=method,
+            params=params,
+            data=data,
+        )
+
+    def _call_project_api_raw(
+        self,
+        *args,
+        add_fork_part: bool = True,
+        add_api_endpoint_part: bool = True,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data: Optional[dict] = None,
+    ) -> RequestResponse:
+        """
+        Call project API endpoint.
+
+        Args:
+            *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+            add_fork_part: If the project is a fork, use `fork/username` prefix.
+
+                Defaults to `True`.
+            add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+                Defaults to `True`.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            `RequestResponse` object containing response.
+        """
+        request_url = self._get_project_url(
+            *args,
+            add_api_endpoint_part=add_api_endpoint_part,
+            add_fork_part=add_fork_part,
+        )
+
+        return self.service.call_api_raw(
+            url=request_url,
+            method=method,
+            params=params,
+            data=data,
+        )
+
+    def _get_project_url(self, *args, add_fork_part=True, add_api_endpoint_part=True):
+        additional_parts = []
+        if self._is_fork and add_fork_part:
+            additional_parts += ["fork", self._user]
+        return self.service.get_api_url(
+            *additional_parts,
+            self.namespace,
+            self.repo,
+            *args,
+            add_api_endpoint_part=add_api_endpoint_part,
+        )
+
+    def get_project_info(self):
+        return self._call_project_api(method="GET")
+
+    def get_branches(self) -> list[str]:
+        return_value = self._call_project_api("git", "branches", method="GET")
+        return return_value["branches"]
+
+    @property
+    def default_branch(self) -> str:
+        return_value = self._call_project_api("git", "branches", method="GET")
+        return return_value["default"]
+
+    def get_description(self) -> str:
+        return self.get_project_info()["description"]
+
+    @property
+    def description(self) -> str:
+        return self.get_project_info()["description"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        raise OperationNotSupported("Not possible on Pagure")
+
+    @property
+    def has_issues(self) -> bool:
+        options = self._call_project_api("options", method="GET")
+        return options["settings"]["issue_tracker"]
+
+    def get_owners(self) -> list[str]:
+        project = self.get_project_info()
+        return project["access_users"]["owner"]
+
+    def who_can_close_issue(self) -> set[str]:
+        users: set[str] = set()
+        project = self.get_project_info()
+        users.update(project["access_users"]["admin"])
+        users.update(project["access_users"]["commit"])
+        users.update(project["access_users"]["ticket"])
+        users.update(project["access_users"]["owner"])
+        return users
+
+    def who_can_merge_pr(self) -> set[str]:
+        users: set[str] = set()
+        project = self.get_project_info()
+        users.update(project["access_users"]["admin"])
+        users.update(project["access_users"]["commit"])
+        users.update(project["access_users"]["owner"])
+        return users
+
+    def which_groups_can_merge_pr(self) -> set[str]:
+        groups: set[str] = set()
+        project = self.get_project_info()
+        groups.update(project["access_groups"]["admin"])
+        groups.update(project["access_groups"]["commit"])
+        return groups
+
+    def can_merge_pr(self, username) -> bool:
+        return username in self.who_can_merge_pr()
+
+    def request_access(self):
+        raise OperationNotSupported("Not possible on Pagure")
+
+    @indirect(PagureIssue.get_list)
+    def get_issue_list(
+        self,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list[Issue]:
+        pass
+
+    @indirect(PagureIssue.get)
+    def get_issue(self, issue_id: int) -> Issue:
+        pass
+
+    def delete(self) -> None:
+        self._call_project_api_raw("delete", method="POST")
+
+    @indirect(PagureIssue.create)
+    def create_issue(
+        self,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> Issue:
+        pass
+
+    @indirect(PagurePullRequest.get_list)
+    def get_pr_list(
+        self,
+        status: PRStatus = PRStatus.open,
+        assignee=None,
+        author=None,
+    ) -> list[PullRequest]:
+        pass
+
+    @indirect(PagurePullRequest.get)
+    def get_pr(self, pr_id: int) -> PullRequest:
+        pass
+
+    @indirect(PagurePullRequest.get_files_diff)
+    def get_pr_files_diff(
+        self,
+        pr_id: int,
+        retries: int = 0,
+        wait_seconds: int = 3,
+    ) -> dict:
+        pass
+
+    @if_readonly(return_function=GitProjectReadOnly.create_pr)
+    @indirect(PagurePullRequest.create)
+    def create_pr(
+        self,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> PullRequest:
+        pass
+
+    @if_readonly(return_function=GitProjectReadOnly.fork_create)
+    def fork_create(self, namespace: Optional[str] = None) -> "PagureProject":
+        if namespace is not None:
+            raise OperationNotSupported(
+                "Pagure does not support forking to namespaces.",
+            )
+
+        request_url = self.service.get_api_url("fork")
+        self.service.call_api(
+            url=request_url,
+            method="POST",
+            data={"repo": self.repo, "namespace": self.namespace, "wait": True},
+        )
+        fork = self._construct_fork_project()
+        logger.debug(f"Forked to {fork.full_repo_name}")
+        return fork
+
+    def _construct_fork_project(self) -> "PagureProject":
+        return PagureProject(
+            service=self.service,
+            repo=self.repo,
+            namespace=self.namespace,
+            username=self._user,
+            is_fork=True,
+        )
+
+    def get_fork(self, create: bool = True) -> Optional["PagureProject"]:
+        if self.is_fork:
+            raise OgrException("Cannot create fork from fork.")
+
+        for fork in self.get_forks():
+            fork_info = fork.get_project_info()
+            if self._user == fork_info["user"]["name"]:
+                return fork
+
+        if not self.is_forked():
+            if create:
+                return self.fork_create()
+
+            logger.info(
+                f"Fork of {self.repo}"
+                " does not exist and we were asked not to create it.",
+            )
+            return None
+        return self._construct_fork_project()
+
+    def exists(self) -> bool:
+        response = self._call_project_api_raw()
+        return response.ok
+
+    def is_private(self) -> bool:
+        host = urlparse(self.service.instance_url).hostname
+        if host in [
+            "git.centos.org",
+            "git.stg.centos.org",
+            "pagure.io",
+            "src.fedoraproject.org",
+            "src.stg.fedoraproject.org",
+        ]:
+            # private repositories are not allowed on generally used pagure instances
+            return False
+        raise OperationNotSupported(
+            f"is_private is not implemented for {self.service.instance_url}."
+            f"Please open issue in https://github.com/packit/ogr",
+        )
+
+    def is_forked(self) -> bool:
+        f = self._construct_fork_project()
+        return bool(f.exists() and f.parent.exists())
+
+    def get_is_fork_from_api(self) -> bool:
+        return bool(self.get_project_info()["parent"])
+
+    @property
+    def is_fork(self) -> bool:
+        return self._is_fork
+
+    @property
+    def parent(self) -> Optional["PagureProject"]:
+        if self.get_is_fork_from_api():
+            return PagureProject(
+                repo=self.repo,
+                namespace=self.get_project_info()["parent"]["namespace"],
+                service=self.service,
+            )
+        return None
+
+    def get_git_urls(self) -> dict[str, str]:
+        return_value = self._call_project_api("git", "urls")
+        return return_value["urls"]
+
+    def add_user(self, user: str, access_level: AccessLevel) -> None:
+        self.add_user_or_group(user, access_level, "user")
+
+    def remove_user(self, user: str) -> None:
+        self.add_user_or_group(user, None, "user")
+
+    def add_group(self, group: str, access_level: AccessLevel):
+        self.add_user_or_group(group, access_level, "group")
+
+    def remove_group(self, group: str) -> None:
+        self.add_user_or_group(group, None, "group")
+
+    def add_user_or_group(
+        self,
+        user: str,
+        access_level: Optional[AccessLevel],
+        user_type: str,
+    ) -> None:
+        access_dict = {
+            AccessLevel.pull: "ticket",
+            AccessLevel.triage: "ticket",
+            AccessLevel.push: "commit",
+            AccessLevel.admin: "commit",
+            AccessLevel.maintain: "admin",
+            None: "",
+        }
+        response = self._call_project_api_raw(
+            "git",
+            "modifyacls",
+            method="POST",
+            data={
+                "user_type": user_type,
+                "name": user,
+                "acl": access_dict[access_level],
+            },
+        )
+
+        if response.status_code == 401:
+            raise PagureAPIException(
+                "You are not allowed to modify ACL's",
+                response_code=response.status_code,
+            )
+
+    def change_token(self, new_token: str) -> None:
+        self.service.change_token(new_token)
+
+    def get_file_content(self, path: str, ref=None) -> str:
+        ref = ref or self.default_branch
+        result = self._call_project_api_raw(
+            "raw",
+            ref,
+            "f",
+            path,
+            add_api_endpoint_part=False,
+        )
+
+        if not result or result.reason == "NOT FOUND":
+            raise FileNotFoundError(f"File '{path}' on {ref} not found")
+        if result.reason != "OK":
+            raise PagureAPIException(
+                f"File '{path}' on {ref} not found due to {result.reason}",
+            )
+        return result.content.decode()
+
+    def get_sha_from_tag(self, tag_name: str) -> str:
+        tags_dict = self.get_tags_dict()
+        if tag_name not in tags_dict:
+            raise PagureAPIException(f"Tag '{tag_name}' not found.", response_code=404)
+
+        return tags_dict[tag_name].commit_sha
+
+    def commit_comment(
+        self,
+        commit: str,
+        body: str,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> CommitComment:
+        raise OperationNotSupported("Commit comments are not supported on Pagure.")
+
+    def get_commit_comments(self, commit: str) -> list[CommitComment]:
+        raise OperationNotSupported("Commit comments are not supported on Pagure.")
+
+    @if_readonly(return_function=GitProjectReadOnly.set_commit_status)
+    @indirect(PagureCommitFlag.set)
+    def set_commit_status(
+        self,
+        commit: str,
+        state: CommitStatus,
+        target_url: str,
+        description: str,
+        context: str,
+        percent: Optional[int] = None,
+        uid: Optional[str] = None,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        pass
+
+    @indirect(PagureCommitFlag.get)
+    def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+        pass
+
+    def get_tags(self) -> list[GitTag]:
+        response = self._call_project_api("git", "tags", params={"with_commits": True})
+        return [GitTag(name=n, commit_sha=c) for n, c in response["tags"].items()]
+
+    def get_tags_dict(self) -> dict[str, GitTag]:
+        response = self._call_project_api("git", "tags", params={"with_commits": True})
+        return {n: GitTag(name=n, commit_sha=c) for n, c in response["tags"].items()}
+
+    @indirect(PagureRelease.get_list)
+    def get_releases(self) -> list[Release]:
+        pass
+
+    @indirect(PagureRelease.get)
+    def get_release(self, identifier=None, name=None, tag_name=None) -> PagureRelease:
+        pass
+
+    @indirect(PagureRelease.get_latest)
+    def get_latest_release(self) -> Optional[PagureRelease]:
+        pass
+
+    @indirect(PagureRelease.create)
+    def create_release(
+        self,
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> Release:
+        pass
+
+    def get_forks(self) -> list["PagureProject"]:
+        forks_url = self.service.get_api_url("projects")
+        projects_response = self.service.call_api(
+            url=forks_url,
+            params={"fork": True, "pattern": self.repo},
+        )
+        return [
+            PagureProject(
+                repo=fork["name"],
+                namespace=fork["namespace"],
+                service=self.service,
+                username=fork["user"]["name"],
+                is_fork=True,
+            )
+            for fork in projects_response["projects"]
+        ]
+
+    def get_web_url(self) -> str:
+        return f'{self.service.instance_url}/{self.get_project_info()["url_path"]}'
+
+    @property
+    def full_repo_name(self) -> str:
+        fork = f"fork/{self._user}/" if self.is_fork else ""
+        namespace = f"{self.namespace}/" if self.namespace else ""
+        return f"{fork}{namespace}{self.repo}"
+
+    def __get_files(
+        self,
+        path: str,
+        ref: Optional[str] = None,
+        recursive: bool = False,
+    ) -> Iterable[str]:
+        subfolders = ["."]
+
+        while subfolders:
+            path = subfolders.pop()
+            split_path = []
+            if path != ".":
+                split_path = ["f", *path.split("/")]
+            response = self._call_project_api("tree", ref, *split_path)
+
+            for file in response["content"]:
+                if file["type"] == "file":
+                    yield file["path"]
+                elif recursive and file["type"] == "folder":
+                    subfolders.append(file["path"])
+
+    def get_files(
+        self,
+        ref: Optional[str] = None,
+        filter_regex: Optional[str] = None,
+        recursive: bool = False,
+    ) -> list[str]:
+        ref = ref or self.default_branch
+        paths = list(self.__get_files(".", ref, recursive))
+        if filter_regex:
+            paths = filter_paths(paths, filter_regex)
+
+        return paths
+
+    def get_sha_from_branch(self, branch: str) -> Optional[str]:
+        branches = self._call_project_api(
+            "git",
+            "branches",
+            params={"with_commits": True},
+        )["branches"]
+
+        return branches.get(branch)
+
+    def get_contributors(self) -> set[str]:
+        raise OperationNotSupported("Pagure doesn't provide list of contributors")
+
+    def users_with_write_access(self) -> set[str]:
+        users_with_access = self.get_project_info()["access_users"]
+        result = set()
+        for access_level in ["commit", "admin", "owner"]:
+            result.update(users_with_access[access_level])
+
+        return result
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class PagureProject +(repo: str, namespace: Optional[str], service: ogr_pagure.PagureService, username: Optional[str] = None, is_fork: bool = False) +
+
+

Args

+
+
repo
+
Name of the project.
+
service
+
GitService instance.
+
namespace
+
+

Namespace of the project.

+
    +
  • GitHub: username or org name.
  • +
  • GitLab: username or org name.
  • +
  • Pagure: namespace (e.g. "rpms").
  • +
+

In case of forks: "fork/{username}/{namespace}".

+
+
+
+ +Expand source code + +
class PagureProject(BaseGitProject):
+    service: "ogr_pagure.PagureService"
+
+    def __init__(
+        self,
+        repo: str,
+        namespace: Optional[str],
+        service: "ogr_pagure.PagureService",
+        username: Optional[str] = None,
+        is_fork: bool = False,
+    ) -> None:
+        super().__init__(repo, service, namespace)
+        self.read_only = service.read_only
+
+        self._is_fork = is_fork
+        self._username = username
+
+        self.repo = repo
+        self.namespace = namespace
+
+    def __str__(self) -> str:
+        fork_info = ""
+        if self._is_fork:
+            fork_info = f', username="{self._username}", is_fork={self._is_fork}'
+        return f'PagureProject(namespace="{self.namespace}", repo="{self.repo}"{fork_info})'
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, PagureProject):
+            return False
+
+        return (
+            self.repo == o.repo
+            and self.namespace == o.namespace
+            and self.service == o.service
+            and self._username == o._username
+            and self._is_fork == o._is_fork
+            and self.read_only == o.read_only
+        )
+
+    @property
+    def _user(self) -> str:
+        if not self._username:
+            self._username = self.service.user.get_username()
+        return self._username
+
+    def _call_project_api(
+        self,
+        *args,
+        add_fork_part: bool = True,
+        add_api_endpoint_part: bool = True,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data: Optional[dict] = None,
+    ) -> dict:
+        """
+        Call project API endpoint.
+
+        Args:
+            *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+            add_fork_part: If the project is a fork, use `fork/username` prefix.
+
+                Defaults to `True`.
+            add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+                Defaults to `True`.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            Dictionary representing response.
+        """
+        request_url = self._get_project_url(
+            *args,
+            add_api_endpoint_part=add_api_endpoint_part,
+            add_fork_part=add_fork_part,
+        )
+
+        return self.service.call_api(
+            url=request_url,
+            method=method,
+            params=params,
+            data=data,
+        )
+
+    def _call_project_api_raw(
+        self,
+        *args,
+        add_fork_part: bool = True,
+        add_api_endpoint_part: bool = True,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data: Optional[dict] = None,
+    ) -> RequestResponse:
+        """
+        Call project API endpoint.
+
+        Args:
+            *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+            add_fork_part: If the project is a fork, use `fork/username` prefix.
+
+                Defaults to `True`.
+            add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+                Defaults to `True`.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            `RequestResponse` object containing response.
+        """
+        request_url = self._get_project_url(
+            *args,
+            add_api_endpoint_part=add_api_endpoint_part,
+            add_fork_part=add_fork_part,
+        )
+
+        return self.service.call_api_raw(
+            url=request_url,
+            method=method,
+            params=params,
+            data=data,
+        )
+
+    def _get_project_url(self, *args, add_fork_part=True, add_api_endpoint_part=True):
+        additional_parts = []
+        if self._is_fork and add_fork_part:
+            additional_parts += ["fork", self._user]
+        return self.service.get_api_url(
+            *additional_parts,
+            self.namespace,
+            self.repo,
+            *args,
+            add_api_endpoint_part=add_api_endpoint_part,
+        )
+
+    def get_project_info(self):
+        return self._call_project_api(method="GET")
+
+    def get_branches(self) -> list[str]:
+        return_value = self._call_project_api("git", "branches", method="GET")
+        return return_value["branches"]
+
+    @property
+    def default_branch(self) -> str:
+        return_value = self._call_project_api("git", "branches", method="GET")
+        return return_value["default"]
+
+    def get_description(self) -> str:
+        return self.get_project_info()["description"]
+
+    @property
+    def description(self) -> str:
+        return self.get_project_info()["description"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        raise OperationNotSupported("Not possible on Pagure")
+
+    @property
+    def has_issues(self) -> bool:
+        options = self._call_project_api("options", method="GET")
+        return options["settings"]["issue_tracker"]
+
+    def get_owners(self) -> list[str]:
+        project = self.get_project_info()
+        return project["access_users"]["owner"]
+
+    def who_can_close_issue(self) -> set[str]:
+        users: set[str] = set()
+        project = self.get_project_info()
+        users.update(project["access_users"]["admin"])
+        users.update(project["access_users"]["commit"])
+        users.update(project["access_users"]["ticket"])
+        users.update(project["access_users"]["owner"])
+        return users
+
+    def who_can_merge_pr(self) -> set[str]:
+        users: set[str] = set()
+        project = self.get_project_info()
+        users.update(project["access_users"]["admin"])
+        users.update(project["access_users"]["commit"])
+        users.update(project["access_users"]["owner"])
+        return users
+
+    def which_groups_can_merge_pr(self) -> set[str]:
+        groups: set[str] = set()
+        project = self.get_project_info()
+        groups.update(project["access_groups"]["admin"])
+        groups.update(project["access_groups"]["commit"])
+        return groups
+
+    def can_merge_pr(self, username) -> bool:
+        return username in self.who_can_merge_pr()
+
+    def request_access(self):
+        raise OperationNotSupported("Not possible on Pagure")
+
+    @indirect(PagureIssue.get_list)
+    def get_issue_list(
+        self,
+        status: IssueStatus = IssueStatus.open,
+        author: Optional[str] = None,
+        assignee: Optional[str] = None,
+        labels: Optional[list[str]] = None,
+    ) -> list[Issue]:
+        pass
+
+    @indirect(PagureIssue.get)
+    def get_issue(self, issue_id: int) -> Issue:
+        pass
+
+    def delete(self) -> None:
+        self._call_project_api_raw("delete", method="POST")
+
+    @indirect(PagureIssue.create)
+    def create_issue(
+        self,
+        title: str,
+        body: str,
+        private: Optional[bool] = None,
+        labels: Optional[list[str]] = None,
+        assignees: Optional[list[str]] = None,
+    ) -> Issue:
+        pass
+
+    @indirect(PagurePullRequest.get_list)
+    def get_pr_list(
+        self,
+        status: PRStatus = PRStatus.open,
+        assignee=None,
+        author=None,
+    ) -> list[PullRequest]:
+        pass
+
+    @indirect(PagurePullRequest.get)
+    def get_pr(self, pr_id: int) -> PullRequest:
+        pass
+
+    @indirect(PagurePullRequest.get_files_diff)
+    def get_pr_files_diff(
+        self,
+        pr_id: int,
+        retries: int = 0,
+        wait_seconds: int = 3,
+    ) -> dict:
+        pass
+
+    @if_readonly(return_function=GitProjectReadOnly.create_pr)
+    @indirect(PagurePullRequest.create)
+    def create_pr(
+        self,
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> PullRequest:
+        pass
+
+    @if_readonly(return_function=GitProjectReadOnly.fork_create)
+    def fork_create(self, namespace: Optional[str] = None) -> "PagureProject":
+        if namespace is not None:
+            raise OperationNotSupported(
+                "Pagure does not support forking to namespaces.",
+            )
+
+        request_url = self.service.get_api_url("fork")
+        self.service.call_api(
+            url=request_url,
+            method="POST",
+            data={"repo": self.repo, "namespace": self.namespace, "wait": True},
+        )
+        fork = self._construct_fork_project()
+        logger.debug(f"Forked to {fork.full_repo_name}")
+        return fork
+
+    def _construct_fork_project(self) -> "PagureProject":
+        return PagureProject(
+            service=self.service,
+            repo=self.repo,
+            namespace=self.namespace,
+            username=self._user,
+            is_fork=True,
+        )
+
+    def get_fork(self, create: bool = True) -> Optional["PagureProject"]:
+        if self.is_fork:
+            raise OgrException("Cannot create fork from fork.")
+
+        for fork in self.get_forks():
+            fork_info = fork.get_project_info()
+            if self._user == fork_info["user"]["name"]:
+                return fork
+
+        if not self.is_forked():
+            if create:
+                return self.fork_create()
+
+            logger.info(
+                f"Fork of {self.repo}"
+                " does not exist and we were asked not to create it.",
+            )
+            return None
+        return self._construct_fork_project()
+
+    def exists(self) -> bool:
+        response = self._call_project_api_raw()
+        return response.ok
+
+    def is_private(self) -> bool:
+        host = urlparse(self.service.instance_url).hostname
+        if host in [
+            "git.centos.org",
+            "git.stg.centos.org",
+            "pagure.io",
+            "src.fedoraproject.org",
+            "src.stg.fedoraproject.org",
+        ]:
+            # private repositories are not allowed on generally used pagure instances
+            return False
+        raise OperationNotSupported(
+            f"is_private is not implemented for {self.service.instance_url}."
+            f"Please open issue in https://github.com/packit/ogr",
+        )
+
+    def is_forked(self) -> bool:
+        f = self._construct_fork_project()
+        return bool(f.exists() and f.parent.exists())
+
+    def get_is_fork_from_api(self) -> bool:
+        return bool(self.get_project_info()["parent"])
+
+    @property
+    def is_fork(self) -> bool:
+        return self._is_fork
+
+    @property
+    def parent(self) -> Optional["PagureProject"]:
+        if self.get_is_fork_from_api():
+            return PagureProject(
+                repo=self.repo,
+                namespace=self.get_project_info()["parent"]["namespace"],
+                service=self.service,
+            )
+        return None
+
+    def get_git_urls(self) -> dict[str, str]:
+        return_value = self._call_project_api("git", "urls")
+        return return_value["urls"]
+
+    def add_user(self, user: str, access_level: AccessLevel) -> None:
+        self.add_user_or_group(user, access_level, "user")
+
+    def remove_user(self, user: str) -> None:
+        self.add_user_or_group(user, None, "user")
+
+    def add_group(self, group: str, access_level: AccessLevel):
+        self.add_user_or_group(group, access_level, "group")
+
+    def remove_group(self, group: str) -> None:
+        self.add_user_or_group(group, None, "group")
+
+    def add_user_or_group(
+        self,
+        user: str,
+        access_level: Optional[AccessLevel],
+        user_type: str,
+    ) -> None:
+        access_dict = {
+            AccessLevel.pull: "ticket",
+            AccessLevel.triage: "ticket",
+            AccessLevel.push: "commit",
+            AccessLevel.admin: "commit",
+            AccessLevel.maintain: "admin",
+            None: "",
+        }
+        response = self._call_project_api_raw(
+            "git",
+            "modifyacls",
+            method="POST",
+            data={
+                "user_type": user_type,
+                "name": user,
+                "acl": access_dict[access_level],
+            },
+        )
+
+        if response.status_code == 401:
+            raise PagureAPIException(
+                "You are not allowed to modify ACL's",
+                response_code=response.status_code,
+            )
+
+    def change_token(self, new_token: str) -> None:
+        self.service.change_token(new_token)
+
+    def get_file_content(self, path: str, ref=None) -> str:
+        ref = ref or self.default_branch
+        result = self._call_project_api_raw(
+            "raw",
+            ref,
+            "f",
+            path,
+            add_api_endpoint_part=False,
+        )
+
+        if not result or result.reason == "NOT FOUND":
+            raise FileNotFoundError(f"File '{path}' on {ref} not found")
+        if result.reason != "OK":
+            raise PagureAPIException(
+                f"File '{path}' on {ref} not found due to {result.reason}",
+            )
+        return result.content.decode()
+
+    def get_sha_from_tag(self, tag_name: str) -> str:
+        tags_dict = self.get_tags_dict()
+        if tag_name not in tags_dict:
+            raise PagureAPIException(f"Tag '{tag_name}' not found.", response_code=404)
+
+        return tags_dict[tag_name].commit_sha
+
+    def commit_comment(
+        self,
+        commit: str,
+        body: str,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> CommitComment:
+        raise OperationNotSupported("Commit comments are not supported on Pagure.")
+
+    def get_commit_comments(self, commit: str) -> list[CommitComment]:
+        raise OperationNotSupported("Commit comments are not supported on Pagure.")
+
+    @if_readonly(return_function=GitProjectReadOnly.set_commit_status)
+    @indirect(PagureCommitFlag.set)
+    def set_commit_status(
+        self,
+        commit: str,
+        state: CommitStatus,
+        target_url: str,
+        description: str,
+        context: str,
+        percent: Optional[int] = None,
+        uid: Optional[str] = None,
+        trim: bool = False,
+    ) -> "CommitFlag":
+        pass
+
+    @indirect(PagureCommitFlag.get)
+    def get_commit_statuses(self, commit: str) -> list[CommitFlag]:
+        pass
+
+    def get_tags(self) -> list[GitTag]:
+        response = self._call_project_api("git", "tags", params={"with_commits": True})
+        return [GitTag(name=n, commit_sha=c) for n, c in response["tags"].items()]
+
+    def get_tags_dict(self) -> dict[str, GitTag]:
+        response = self._call_project_api("git", "tags", params={"with_commits": True})
+        return {n: GitTag(name=n, commit_sha=c) for n, c in response["tags"].items()}
+
+    @indirect(PagureRelease.get_list)
+    def get_releases(self) -> list[Release]:
+        pass
+
+    @indirect(PagureRelease.get)
+    def get_release(self, identifier=None, name=None, tag_name=None) -> PagureRelease:
+        pass
+
+    @indirect(PagureRelease.get_latest)
+    def get_latest_release(self) -> Optional[PagureRelease]:
+        pass
+
+    @indirect(PagureRelease.create)
+    def create_release(
+        self,
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> Release:
+        pass
+
+    def get_forks(self) -> list["PagureProject"]:
+        forks_url = self.service.get_api_url("projects")
+        projects_response = self.service.call_api(
+            url=forks_url,
+            params={"fork": True, "pattern": self.repo},
+        )
+        return [
+            PagureProject(
+                repo=fork["name"],
+                namespace=fork["namespace"],
+                service=self.service,
+                username=fork["user"]["name"],
+                is_fork=True,
+            )
+            for fork in projects_response["projects"]
+        ]
+
+    def get_web_url(self) -> str:
+        return f'{self.service.instance_url}/{self.get_project_info()["url_path"]}'
+
+    @property
+    def full_repo_name(self) -> str:
+        fork = f"fork/{self._user}/" if self.is_fork else ""
+        namespace = f"{self.namespace}/" if self.namespace else ""
+        return f"{fork}{namespace}{self.repo}"
+
+    def __get_files(
+        self,
+        path: str,
+        ref: Optional[str] = None,
+        recursive: bool = False,
+    ) -> Iterable[str]:
+        subfolders = ["."]
+
+        while subfolders:
+            path = subfolders.pop()
+            split_path = []
+            if path != ".":
+                split_path = ["f", *path.split("/")]
+            response = self._call_project_api("tree", ref, *split_path)
+
+            for file in response["content"]:
+                if file["type"] == "file":
+                    yield file["path"]
+                elif recursive and file["type"] == "folder":
+                    subfolders.append(file["path"])
+
+    def get_files(
+        self,
+        ref: Optional[str] = None,
+        filter_regex: Optional[str] = None,
+        recursive: bool = False,
+    ) -> list[str]:
+        ref = ref or self.default_branch
+        paths = list(self.__get_files(".", ref, recursive))
+        if filter_regex:
+            paths = filter_paths(paths, filter_regex)
+
+        return paths
+
+    def get_sha_from_branch(self, branch: str) -> Optional[str]:
+        branches = self._call_project_api(
+            "git",
+            "branches",
+            params={"with_commits": True},
+        )["branches"]
+
+        return branches.get(branch)
+
+    def get_contributors(self) -> set[str]:
+        raise OperationNotSupported("Pagure doesn't provide list of contributors")
+
+    def users_with_write_access(self) -> set[str]:
+        users_with_access = self.get_project_info()["access_users"]
+        result = set()
+        for access_level in ["commit", "admin", "owner"]:
+            result.update(users_with_access[access_level])
+
+        return result
+
+

Ancestors

+ +

Class variables

+
+
var servicePagureService
+
+
+
+
+

Methods

+
+
+def add_user_or_group(self, user: str, access_level: Optional[AccessLevel], user_type: str) ‑> None +
+
+
+
+ +Expand source code + +
def add_user_or_group(
+    self,
+    user: str,
+    access_level: Optional[AccessLevel],
+    user_type: str,
+) -> None:
+    access_dict = {
+        AccessLevel.pull: "ticket",
+        AccessLevel.triage: "ticket",
+        AccessLevel.push: "commit",
+        AccessLevel.admin: "commit",
+        AccessLevel.maintain: "admin",
+        None: "",
+    }
+    response = self._call_project_api_raw(
+        "git",
+        "modifyacls",
+        method="POST",
+        data={
+            "user_type": user_type,
+            "name": user,
+            "acl": access_dict[access_level],
+        },
+    )
+
+    if response.status_code == 401:
+        raise PagureAPIException(
+            "You are not allowed to modify ACL's",
+            response_code=response.status_code,
+        )
+
+
+
+def get_is_fork_from_api(self) ‑> bool +
+
+
+
+ +Expand source code + +
def get_is_fork_from_api(self) -> bool:
+    return bool(self.get_project_info()["parent"])
+
+
+
+def get_project_info(self) +
+
+
+
+ +Expand source code + +
def get_project_info(self):
+    return self._call_project_api(method="GET")
+
+
+
+def get_tags_dict(self) ‑> dict[str, GitTag] +
+
+
+
+ +Expand source code + +
def get_tags_dict(self) -> dict[str, GitTag]:
+    response = self._call_project_api("git", "tags", params={"with_commits": True})
+    return {n: GitTag(name=n, commit_sha=c) for n, c in response["tags"].items()}
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/pagure/pull_request.html b/docs/services/pagure/pull_request.html new file mode 100644 index 00000000..e9c1e614 --- /dev/null +++ b/docs/services/pagure/pull_request.html @@ -0,0 +1,1042 @@ + + + + + + +ogr.services.pagure.pull_request API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.pagure.pull_request

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+import logging
+from time import sleep
+from typing import Any, Optional, Union
+
+from ogr.abstract import CommitFlag, CommitStatus, PRComment, PRStatus, PullRequest
+from ogr.exceptions import PagureAPIException
+from ogr.services import pagure as ogr_pagure
+from ogr.services.base import BasePullRequest
+from ogr.services.pagure.comments import PagurePRComment
+
+logger = logging.getLogger(__name__)
+
+
+class PagurePullRequest(BasePullRequest):
+    _target_project: "ogr_pagure.PagureProject"
+    _source_project: "ogr_pagure.PagureProject" = None
+
+    def __init__(self, raw_pr, project):
+        super().__init__(raw_pr, project)
+        self.__dirty = False
+
+    def __update(self):
+        if self.__dirty:
+            self._raw_pr = self.__call_api()
+            self.__dirty = False
+
+    @property
+    def title(self) -> str:
+        self.__update()
+        return self._raw_pr["title"]
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self.update_info(title=new_title)
+
+    @property
+    def id(self) -> int:
+        return self._raw_pr["id"]
+
+    @property
+    def status(self) -> PRStatus:
+        self.__update()
+        return PRStatus[self._raw_pr["status"].lower()]
+
+    @property
+    def url(self) -> str:
+        return "/".join(
+            [
+                self.target_project.service.instance_url,
+                self._raw_pr["project"]["url_path"],
+                "pull-request",
+                str(self.id),
+            ],
+        )
+
+    @property
+    def description(self) -> str:
+        self.__update()
+        return self._raw_pr["initial_comment"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.update_info(description=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_pr["user"]["name"]
+
+    @property
+    def source_branch(self) -> str:
+        return self._raw_pr["branch_from"]
+
+    @property
+    def target_branch(self) -> str:
+        return self._raw_pr["branch"]
+
+    @property
+    def created(self) -> datetime.datetime:
+        return datetime.datetime.fromtimestamp(int(self._raw_pr["date_created"]))
+
+    @property
+    def diff_url(self) -> str:
+        return f"{self.url}#request_diff"
+
+    @property
+    def commits_url(self) -> str:
+        return f"{self.url}#commit_list"
+
+    @property
+    def patch(self) -> bytes:
+        request_response = self._target_project._call_project_api_raw(
+            "pull-request",
+            f"{self.id}.patch",
+            add_api_endpoint_part=False,
+        )
+        if request_response.status_code != 200:
+            raise PagureAPIException(
+                f"Cannot get patch from {self.url}.patch because {request_response.reason}.",
+                response_code=request_response.status_code,
+            )
+        return request_response.content
+
+    @property
+    def head_commit(self) -> str:
+        return self._raw_pr["commit_stop"]
+
+    @property
+    def source_project(self) -> "ogr_pagure.PagureProject":
+        if self._source_project is None:
+            source = self._raw_pr["repo_from"]
+            source_project_info = {
+                "repo": source["name"],
+                "namespace": source["namespace"],
+            }
+
+            if source["parent"] is not None:
+                source_project_info["is_fork"] = True
+                source_project_info["username"] = source["user"]["name"]
+
+            self._source_project = self._target_project.service.get_project(
+                **source_project_info,
+            )
+
+        return self._source_project
+
+    @property
+    def closed_by(self) -> Optional[str]:
+        closed_by = self._raw_pr["closed_by"]
+        return closed_by["name"] if closed_by else None
+
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+    def __call_api(self, *args, **kwargs) -> dict:
+        return self._target_project._call_project_api(
+            "pull-request",
+            str(self.id),
+            *args,
+            **kwargs,
+        )
+
+    @staticmethod
+    def create(
+        project: "ogr_pagure.PagureProject",
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        data = {
+            "title": title,
+            "branch_to": target_branch,
+            "branch_from": source_branch,
+            "initial_comment": body,
+        }
+
+        caller = project
+        if project.is_fork:
+            data["repo_from"] = project.repo
+            data["repo_from_username"] = project._user
+            data["repo_from_namespace"] = project.namespace
+
+            # running the call from the parent project
+            caller = caller.parent
+        elif fork_username:
+            fork_project = project.service.get_project(
+                username=fork_username,
+                repo=project.repo,
+                namespace=project.namespace,
+                is_fork=True,
+            )
+            data["repo_from_username"] = fork_username
+            data["repo_from"] = fork_project.repo
+            data["repo_from_namespace"] = fork_project.namespace
+
+        response = caller._call_project_api(
+            "pull-request",
+            "new",
+            method="POST",
+            data=data,
+        )
+        return PagurePullRequest(response, caller)
+
+    @staticmethod
+    def get(project: "ogr_pagure.PagureProject", pr_id: int) -> "PullRequest":
+        raw_pr = project._call_project_api("pull-request", str(pr_id))
+        return PagurePullRequest(raw_pr, project)
+
+    @staticmethod
+    def get_files_diff(
+        project: "ogr_pagure.PagureProject",
+        pr_id: int,
+        retries: int = 0,
+        wait_seconds: int = 3,
+    ) -> dict:
+        """
+        Retrieve pull request diff statistics.
+
+        Pagure API tends to return ENOPRSTATS error when a pull request is transitioning
+        from open to other states, so you can use `retries` and `wait_seconds` to try to
+        mitigate that.
+
+
+        Args:
+            project: Pagure project.
+            pr_id: Pull request ID.
+            retries: Number of extra attempts.
+            wait_seconds: Delay between attempts.
+        """
+        attempt = 1
+        while True:
+            try:
+                return project._call_project_api(
+                    "pull-request",
+                    str(pr_id),
+                    "diffstats",
+                    method="GET",
+                )
+            except PagureAPIException as ex:  # noqa PERF203
+                if "No statistics" in ex.pagure_error:
+                    # this may be a race condition, try once more
+                    logger.info(
+                        f"While retrieving PR diffstats Pagure returned ENOPRSTATS.\n{ex}",
+                    )
+                    if attempt <= retries:
+                        attempt += 1
+                        logger.info(
+                            f"Trying again; attempt={attempt} after {wait_seconds} seconds",
+                        )
+                        sleep(wait_seconds)
+                        continue
+                raise ex
+
+    @staticmethod
+    def get_list(
+        project: "ogr_pagure.PagureProject",
+        status: PRStatus = PRStatus.open,
+        assignee=None,
+        author=None,
+    ) -> list["PullRequest"]:
+        payload = {"page": 1, "status": status.name.capitalize()}
+        if assignee is not None:
+            payload["assignee"] = assignee
+        if author is not None:
+            payload["author"] = author
+
+        raw_prs = []
+        while True:
+            page_result = project._call_project_api("pull-requests", params=payload)
+            raw_prs += page_result["requests"]
+            if not page_result["pagination"]["next"]:
+                break
+
+            # mypy don't know that key "page" really contains int...
+            payload["page"] += 1  # type: ignore
+
+        return [PagurePullRequest(pr_dict, project) for pr_dict in raw_prs]
+
+    def update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "PullRequest":
+        try:
+            data = {"title": title if title else self.title}
+
+            if description:
+                data["initial_comment"] = description
+
+            updated_pr = self.__call_api(method="POST", data=data)
+            logger.info("PR updated.")
+
+            self._raw_pr = updated_pr
+            return self
+        except Exception as ex:
+            raise PagureAPIException("there was an error while updating the PR") from ex
+
+    def _get_all_comments(self) -> list[PRComment]:
+        self.__update()
+        raw_comments = self._raw_pr["comments"]
+        return [
+            PagurePRComment(parent=self, raw_comment=comment_dict)
+            for comment_dict in raw_comments
+        ]
+
+    def comment(
+        self,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        payload: dict[str, Any] = {"comment": body}
+        if commit is not None:
+            payload["commit"] = commit
+        if filename is not None:
+            payload["filename"] = filename
+        if row is not None:
+            payload["row"] = row
+
+        self.__call_api("comment", method="POST", data=payload)
+        self.__dirty = True
+        return PagurePRComment(
+            parent=self,
+            body=body,
+            author=self.target_project.service.user.get_username(),
+        )
+
+    def close(self) -> "PullRequest":
+        return_value = self.__call_api("close", method="POST")
+
+        if return_value["message"] != "Pull-request closed!":
+            raise PagureAPIException(return_value["message"])
+
+        self.__dirty = True
+        return self
+
+    def merge(self) -> "PullRequest":
+        return_value = self.__call_api("merge", method="POST")
+
+        if return_value["message"] != "Changes merged!":
+            raise PagureAPIException(return_value["message"])
+
+        self.__dirty = True
+        return self
+
+    def get_statuses(self) -> list[CommitFlag]:
+        self.__update()
+        return self.target_project.get_commit_statuses(self._raw_pr["commit_stop"])
+
+    def set_flag(
+        self,
+        username: str,
+        comment: str,
+        url: str,
+        status: Optional[CommitStatus] = None,
+        percent: Optional[int] = None,
+        uid: Optional[str] = None,
+    ) -> dict:
+        """
+        Set a flag on a pull-request to display results or status of CI tasks.
+
+        See "Flag a pull-request" at https://pagure.io/api/0/#pull_requests-tab
+        for a full description of the parameters.
+
+        Args:
+            username: The name of the application to be presented to users
+                on the pull request page.
+            comment: A short message summarizing the presented results.
+            url: A URL to the result of this flag.
+            status: The status to be displayed for this flag.
+            percent: A percentage of completion compared to the goal.
+            uid: A unique identifier used to identify a flag on the pull-request.
+
+        Returns:
+            Dictionary with the response received from Pagure.
+        """
+        data: dict[str, Union[str, int]] = {
+            "username": username,
+            "comment": comment,
+            "url": url,
+        }
+        if status is not None:
+            data["status"] = status.name
+        if percent is not None:
+            data["percent"] = percent
+        if uid is not None:
+            data["uid"] = uid
+        return self.__call_api("flag", method="POST", data=data)
+
+    def get_comment(self, comment_id: int) -> PRComment:
+        for comment in self._get_all_comments():
+            if comment.id == comment_id:
+                return comment
+
+        raise PagureAPIException(
+            f"No comment with id#{comment_id} in PR#{self.id} found.",
+            response_code=404,
+        )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class PagurePullRequest +(raw_pr, project) +
+
+

Attributes

+
+
project : GitProject
+
Project of the pull request.
+
+
+ +Expand source code + +
class PagurePullRequest(BasePullRequest):
+    _target_project: "ogr_pagure.PagureProject"
+    _source_project: "ogr_pagure.PagureProject" = None
+
+    def __init__(self, raw_pr, project):
+        super().__init__(raw_pr, project)
+        self.__dirty = False
+
+    def __update(self):
+        if self.__dirty:
+            self._raw_pr = self.__call_api()
+            self.__dirty = False
+
+    @property
+    def title(self) -> str:
+        self.__update()
+        return self._raw_pr["title"]
+
+    @title.setter
+    def title(self, new_title: str) -> None:
+        self.update_info(title=new_title)
+
+    @property
+    def id(self) -> int:
+        return self._raw_pr["id"]
+
+    @property
+    def status(self) -> PRStatus:
+        self.__update()
+        return PRStatus[self._raw_pr["status"].lower()]
+
+    @property
+    def url(self) -> str:
+        return "/".join(
+            [
+                self.target_project.service.instance_url,
+                self._raw_pr["project"]["url_path"],
+                "pull-request",
+                str(self.id),
+            ],
+        )
+
+    @property
+    def description(self) -> str:
+        self.__update()
+        return self._raw_pr["initial_comment"]
+
+    @description.setter
+    def description(self, new_description: str) -> None:
+        self.update_info(description=new_description)
+
+    @property
+    def author(self) -> str:
+        return self._raw_pr["user"]["name"]
+
+    @property
+    def source_branch(self) -> str:
+        return self._raw_pr["branch_from"]
+
+    @property
+    def target_branch(self) -> str:
+        return self._raw_pr["branch"]
+
+    @property
+    def created(self) -> datetime.datetime:
+        return datetime.datetime.fromtimestamp(int(self._raw_pr["date_created"]))
+
+    @property
+    def diff_url(self) -> str:
+        return f"{self.url}#request_diff"
+
+    @property
+    def commits_url(self) -> str:
+        return f"{self.url}#commit_list"
+
+    @property
+    def patch(self) -> bytes:
+        request_response = self._target_project._call_project_api_raw(
+            "pull-request",
+            f"{self.id}.patch",
+            add_api_endpoint_part=False,
+        )
+        if request_response.status_code != 200:
+            raise PagureAPIException(
+                f"Cannot get patch from {self.url}.patch because {request_response.reason}.",
+                response_code=request_response.status_code,
+            )
+        return request_response.content
+
+    @property
+    def head_commit(self) -> str:
+        return self._raw_pr["commit_stop"]
+
+    @property
+    def source_project(self) -> "ogr_pagure.PagureProject":
+        if self._source_project is None:
+            source = self._raw_pr["repo_from"]
+            source_project_info = {
+                "repo": source["name"],
+                "namespace": source["namespace"],
+            }
+
+            if source["parent"] is not None:
+                source_project_info["is_fork"] = True
+                source_project_info["username"] = source["user"]["name"]
+
+            self._source_project = self._target_project.service.get_project(
+                **source_project_info,
+            )
+
+        return self._source_project
+
+    @property
+    def closed_by(self) -> Optional[str]:
+        closed_by = self._raw_pr["closed_by"]
+        return closed_by["name"] if closed_by else None
+
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+    def __call_api(self, *args, **kwargs) -> dict:
+        return self._target_project._call_project_api(
+            "pull-request",
+            str(self.id),
+            *args,
+            **kwargs,
+        )
+
+    @staticmethod
+    def create(
+        project: "ogr_pagure.PagureProject",
+        title: str,
+        body: str,
+        target_branch: str,
+        source_branch: str,
+        fork_username: Optional[str] = None,
+    ) -> "PullRequest":
+        data = {
+            "title": title,
+            "branch_to": target_branch,
+            "branch_from": source_branch,
+            "initial_comment": body,
+        }
+
+        caller = project
+        if project.is_fork:
+            data["repo_from"] = project.repo
+            data["repo_from_username"] = project._user
+            data["repo_from_namespace"] = project.namespace
+
+            # running the call from the parent project
+            caller = caller.parent
+        elif fork_username:
+            fork_project = project.service.get_project(
+                username=fork_username,
+                repo=project.repo,
+                namespace=project.namespace,
+                is_fork=True,
+            )
+            data["repo_from_username"] = fork_username
+            data["repo_from"] = fork_project.repo
+            data["repo_from_namespace"] = fork_project.namespace
+
+        response = caller._call_project_api(
+            "pull-request",
+            "new",
+            method="POST",
+            data=data,
+        )
+        return PagurePullRequest(response, caller)
+
+    @staticmethod
+    def get(project: "ogr_pagure.PagureProject", pr_id: int) -> "PullRequest":
+        raw_pr = project._call_project_api("pull-request", str(pr_id))
+        return PagurePullRequest(raw_pr, project)
+
+    @staticmethod
+    def get_files_diff(
+        project: "ogr_pagure.PagureProject",
+        pr_id: int,
+        retries: int = 0,
+        wait_seconds: int = 3,
+    ) -> dict:
+        """
+        Retrieve pull request diff statistics.
+
+        Pagure API tends to return ENOPRSTATS error when a pull request is transitioning
+        from open to other states, so you can use `retries` and `wait_seconds` to try to
+        mitigate that.
+
+
+        Args:
+            project: Pagure project.
+            pr_id: Pull request ID.
+            retries: Number of extra attempts.
+            wait_seconds: Delay between attempts.
+        """
+        attempt = 1
+        while True:
+            try:
+                return project._call_project_api(
+                    "pull-request",
+                    str(pr_id),
+                    "diffstats",
+                    method="GET",
+                )
+            except PagureAPIException as ex:  # noqa PERF203
+                if "No statistics" in ex.pagure_error:
+                    # this may be a race condition, try once more
+                    logger.info(
+                        f"While retrieving PR diffstats Pagure returned ENOPRSTATS.\n{ex}",
+                    )
+                    if attempt <= retries:
+                        attempt += 1
+                        logger.info(
+                            f"Trying again; attempt={attempt} after {wait_seconds} seconds",
+                        )
+                        sleep(wait_seconds)
+                        continue
+                raise ex
+
+    @staticmethod
+    def get_list(
+        project: "ogr_pagure.PagureProject",
+        status: PRStatus = PRStatus.open,
+        assignee=None,
+        author=None,
+    ) -> list["PullRequest"]:
+        payload = {"page": 1, "status": status.name.capitalize()}
+        if assignee is not None:
+            payload["assignee"] = assignee
+        if author is not None:
+            payload["author"] = author
+
+        raw_prs = []
+        while True:
+            page_result = project._call_project_api("pull-requests", params=payload)
+            raw_prs += page_result["requests"]
+            if not page_result["pagination"]["next"]:
+                break
+
+            # mypy don't know that key "page" really contains int...
+            payload["page"] += 1  # type: ignore
+
+        return [PagurePullRequest(pr_dict, project) for pr_dict in raw_prs]
+
+    def update_info(
+        self,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> "PullRequest":
+        try:
+            data = {"title": title if title else self.title}
+
+            if description:
+                data["initial_comment"] = description
+
+            updated_pr = self.__call_api(method="POST", data=data)
+            logger.info("PR updated.")
+
+            self._raw_pr = updated_pr
+            return self
+        except Exception as ex:
+            raise PagureAPIException("there was an error while updating the PR") from ex
+
+    def _get_all_comments(self) -> list[PRComment]:
+        self.__update()
+        raw_comments = self._raw_pr["comments"]
+        return [
+            PagurePRComment(parent=self, raw_comment=comment_dict)
+            for comment_dict in raw_comments
+        ]
+
+    def comment(
+        self,
+        body: str,
+        commit: Optional[str] = None,
+        filename: Optional[str] = None,
+        row: Optional[int] = None,
+    ) -> "PRComment":
+        payload: dict[str, Any] = {"comment": body}
+        if commit is not None:
+            payload["commit"] = commit
+        if filename is not None:
+            payload["filename"] = filename
+        if row is not None:
+            payload["row"] = row
+
+        self.__call_api("comment", method="POST", data=payload)
+        self.__dirty = True
+        return PagurePRComment(
+            parent=self,
+            body=body,
+            author=self.target_project.service.user.get_username(),
+        )
+
+    def close(self) -> "PullRequest":
+        return_value = self.__call_api("close", method="POST")
+
+        if return_value["message"] != "Pull-request closed!":
+            raise PagureAPIException(return_value["message"])
+
+        self.__dirty = True
+        return self
+
+    def merge(self) -> "PullRequest":
+        return_value = self.__call_api("merge", method="POST")
+
+        if return_value["message"] != "Changes merged!":
+            raise PagureAPIException(return_value["message"])
+
+        self.__dirty = True
+        return self
+
+    def get_statuses(self) -> list[CommitFlag]:
+        self.__update()
+        return self.target_project.get_commit_statuses(self._raw_pr["commit_stop"])
+
+    def set_flag(
+        self,
+        username: str,
+        comment: str,
+        url: str,
+        status: Optional[CommitStatus] = None,
+        percent: Optional[int] = None,
+        uid: Optional[str] = None,
+    ) -> dict:
+        """
+        Set a flag on a pull-request to display results or status of CI tasks.
+
+        See "Flag a pull-request" at https://pagure.io/api/0/#pull_requests-tab
+        for a full description of the parameters.
+
+        Args:
+            username: The name of the application to be presented to users
+                on the pull request page.
+            comment: A short message summarizing the presented results.
+            url: A URL to the result of this flag.
+            status: The status to be displayed for this flag.
+            percent: A percentage of completion compared to the goal.
+            uid: A unique identifier used to identify a flag on the pull-request.
+
+        Returns:
+            Dictionary with the response received from Pagure.
+        """
+        data: dict[str, Union[str, int]] = {
+            "username": username,
+            "comment": comment,
+            "url": url,
+        }
+        if status is not None:
+            data["status"] = status.name
+        if percent is not None:
+            data["percent"] = percent
+        if uid is not None:
+            data["uid"] = uid
+        return self.__call_api("flag", method="POST", data=data)
+
+    def get_comment(self, comment_id: int) -> PRComment:
+        for comment in self._get_all_comments():
+            if comment.id == comment_id:
+                return comment
+
+        raise PagureAPIException(
+            f"No comment with id#{comment_id} in PR#{self.id} found.",
+            response_code=404,
+        )
+
+

Ancestors

+ +

Static methods

+
+
+def get_files_diff(project: ogr_pagure.PagureProject, pr_id: int, retries: int = 0, wait_seconds: int = 3) ‑> dict +
+
+

Retrieve pull request diff statistics.

+

Pagure API tends to return ENOPRSTATS error when a pull request is transitioning +from open to other states, so you can use retries and wait_seconds to try to +mitigate that.

+

Args

+
+
project
+
Pagure project.
+
pr_id
+
Pull request ID.
+
retries
+
Number of extra attempts.
+
wait_seconds
+
Delay between attempts.
+
+
+ +Expand source code + +
@staticmethod
+def get_files_diff(
+    project: "ogr_pagure.PagureProject",
+    pr_id: int,
+    retries: int = 0,
+    wait_seconds: int = 3,
+) -> dict:
+    """
+    Retrieve pull request diff statistics.
+
+    Pagure API tends to return ENOPRSTATS error when a pull request is transitioning
+    from open to other states, so you can use `retries` and `wait_seconds` to try to
+    mitigate that.
+
+
+    Args:
+        project: Pagure project.
+        pr_id: Pull request ID.
+        retries: Number of extra attempts.
+        wait_seconds: Delay between attempts.
+    """
+    attempt = 1
+    while True:
+        try:
+            return project._call_project_api(
+                "pull-request",
+                str(pr_id),
+                "diffstats",
+                method="GET",
+            )
+        except PagureAPIException as ex:  # noqa PERF203
+            if "No statistics" in ex.pagure_error:
+                # this may be a race condition, try once more
+                logger.info(
+                    f"While retrieving PR diffstats Pagure returned ENOPRSTATS.\n{ex}",
+                )
+                if attempt <= retries:
+                    attempt += 1
+                    logger.info(
+                        f"Trying again; attempt={attempt} after {wait_seconds} seconds",
+                    )
+                    sleep(wait_seconds)
+                    continue
+            raise ex
+
+
+
+

Methods

+
+
+def set_flag(self, username: str, comment: str, url: str, status: Optional[CommitStatus] = None, percent: Optional[int] = None, uid: Optional[str] = None) ‑> dict +
+
+

Set a flag on a pull-request to display results or status of CI tasks.

+

See "Flag a pull-request" at https://pagure.io/api/0/#pull_requests-tab +for a full description of the parameters.

+

Args

+
+
username
+
The name of the application to be presented to users +on the pull request page.
+
comment
+
A short message summarizing the presented results.
+
url
+
A URL to the result of this flag.
+
status
+
The status to be displayed for this flag.
+
percent
+
A percentage of completion compared to the goal.
+
uid
+
A unique identifier used to identify a flag on the pull-request.
+
+

Returns

+

Dictionary with the response received from Pagure.

+
+ +Expand source code + +
def set_flag(
+    self,
+    username: str,
+    comment: str,
+    url: str,
+    status: Optional[CommitStatus] = None,
+    percent: Optional[int] = None,
+    uid: Optional[str] = None,
+) -> dict:
+    """
+    Set a flag on a pull-request to display results or status of CI tasks.
+
+    See "Flag a pull-request" at https://pagure.io/api/0/#pull_requests-tab
+    for a full description of the parameters.
+
+    Args:
+        username: The name of the application to be presented to users
+            on the pull request page.
+        comment: A short message summarizing the presented results.
+        url: A URL to the result of this flag.
+        status: The status to be displayed for this flag.
+        percent: A percentage of completion compared to the goal.
+        uid: A unique identifier used to identify a flag on the pull-request.
+
+    Returns:
+        Dictionary with the response received from Pagure.
+    """
+    data: dict[str, Union[str, int]] = {
+        "username": username,
+        "comment": comment,
+        "url": url,
+    }
+    if status is not None:
+        data["status"] = status.name
+    if percent is not None:
+        data["percent"] = percent
+    if uid is not None:
+        data["uid"] = uid
+    return self.__call_api("flag", method="POST", data=data)
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/pagure/release.html b/docs/services/pagure/release.html new file mode 100644 index 00000000..bd963089 --- /dev/null +++ b/docs/services/pagure/release.html @@ -0,0 +1,285 @@ + + + + + + +ogr.services.pagure.release API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.pagure.release

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import datetime
+from typing import Optional
+
+from ogr.abstract import GitTag, Release
+from ogr.exceptions import OperationNotSupported, PagureAPIException
+from ogr.services import pagure as ogr_pagure
+
+
+class PagureRelease(Release):
+    _raw_release: GitTag
+    project: "ogr_pagure.PagureProject"
+
+    @property
+    def title(self):
+        return self.git_tag.name
+
+    @property
+    def body(self):
+        return ""
+
+    @property
+    def git_tag(self) -> GitTag:
+        return self._raw_release
+
+    @property
+    def tag_name(self) -> str:
+        return self._raw_release.name
+
+    @property
+    def url(self) -> Optional[str]:
+        return ""
+
+    @property
+    def created_at(self) -> datetime.datetime:
+        return None
+
+    @property
+    def tarball_url(self) -> str:
+        return ""
+
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+    @staticmethod
+    def get(
+        project: "ogr_pagure.PagureProject",
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> "Release":
+        raise OperationNotSupported()
+
+    @staticmethod
+    def get_latest(project: "ogr_pagure.PagureProject") -> Optional["Release"]:
+        raise OperationNotSupported("Pagure API does not provide timestamps")
+
+    @staticmethod
+    def get_list(project: "ogr_pagure.PagureProject") -> list["Release"]:
+        # git tag for Pagure is shown as Release in Pagure UI
+        git_tags = project.get_tags()
+        return [PagureRelease(git_tag, project) for git_tag in git_tags]
+
+    @staticmethod
+    def create(
+        project: "ogr_pagure.PagureProject",
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> "Release":
+        payload = {
+            "tagname": tag,
+            "commit_hash": ref,
+        }
+        if message:
+            payload["message"] = message
+
+        response = project._call_project_api("git", "tags", data=payload, method="POST")
+        if not response["tag_created"]:
+            raise PagureAPIException("Release has not been created")
+
+        return PagureRelease(GitTag(tag, ref), project)
+
+    def edit_release(self, name: str, message: str) -> None:
+        raise OperationNotSupported("edit_release not supported on Pagure")
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class PagureRelease +(raw_release: Any, project: GitProject) +
+
+

Object that represents release.

+

Attributes

+
+
project : GitProject
+
Project on which the release is created.
+
+
+ +Expand source code + +
class PagureRelease(Release):
+    _raw_release: GitTag
+    project: "ogr_pagure.PagureProject"
+
+    @property
+    def title(self):
+        return self.git_tag.name
+
+    @property
+    def body(self):
+        return ""
+
+    @property
+    def git_tag(self) -> GitTag:
+        return self._raw_release
+
+    @property
+    def tag_name(self) -> str:
+        return self._raw_release.name
+
+    @property
+    def url(self) -> Optional[str]:
+        return ""
+
+    @property
+    def created_at(self) -> datetime.datetime:
+        return None
+
+    @property
+    def tarball_url(self) -> str:
+        return ""
+
+    def __str__(self) -> str:
+        return "Pagure" + super().__str__()
+
+    @staticmethod
+    def get(
+        project: "ogr_pagure.PagureProject",
+        identifier: Optional[int] = None,
+        name: Optional[str] = None,
+        tag_name: Optional[str] = None,
+    ) -> "Release":
+        raise OperationNotSupported()
+
+    @staticmethod
+    def get_latest(project: "ogr_pagure.PagureProject") -> Optional["Release"]:
+        raise OperationNotSupported("Pagure API does not provide timestamps")
+
+    @staticmethod
+    def get_list(project: "ogr_pagure.PagureProject") -> list["Release"]:
+        # git tag for Pagure is shown as Release in Pagure UI
+        git_tags = project.get_tags()
+        return [PagureRelease(git_tag, project) for git_tag in git_tags]
+
+    @staticmethod
+    def create(
+        project: "ogr_pagure.PagureProject",
+        tag: str,
+        name: str,
+        message: str,
+        ref: Optional[str] = None,
+    ) -> "Release":
+        payload = {
+            "tagname": tag,
+            "commit_hash": ref,
+        }
+        if message:
+            payload["message"] = message
+
+        response = project._call_project_api("git", "tags", data=payload, method="POST")
+        if not response["tag_created"]:
+            raise PagureAPIException("Release has not been created")
+
+        return PagureRelease(GitTag(tag, ref), project)
+
+    def edit_release(self, name: str, message: str) -> None:
+        raise OperationNotSupported("edit_release not supported on Pagure")
+
+

Ancestors

+ +

Class variables

+
+
var projectPagureProject
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/pagure/service.html b/docs/services/pagure/service.html new file mode 100644 index 00000000..53f1ad72 --- /dev/null +++ b/docs/services/pagure/service.html @@ -0,0 +1,1187 @@ + + + + + + +ogr.services.pagure.service API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.pagure.service

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import logging
+from typing import Optional, Union
+
+import requests
+import urllib3
+
+from ogr.exceptions import (
+    GitForgeInternalError,
+    OgrException,
+    OgrNetworkError,
+    OperationNotSupported,
+    PagureAPIException,
+)
+from ogr.factory import use_for_service
+from ogr.parsing import parse_git_repo
+from ogr.services.base import BaseGitService, GitProject
+from ogr.services.pagure.project import PagureProject
+from ogr.services.pagure.user import PagureUser
+from ogr.utils import RequestResponse
+
+logger = logging.getLogger(__name__)
+
+
+@use_for_service("pagure")
+@use_for_service("src.fedoraproject.org")
+@use_for_service("src.stg.fedoraproject.org")
+@use_for_service("pkgs.fedoraproject.org")
+@use_for_service("pkgs.stg.fedoraproject.org")
+@use_for_service("git.centos.org")
+@use_for_service("git.stg.centos.org")
+class PagureService(BaseGitService):
+    def __init__(
+        self,
+        token: Optional[str] = None,
+        instance_url: str = "https://src.fedoraproject.org",
+        read_only: bool = False,
+        insecure: bool = False,
+        max_retries: Union[int, urllib3.util.Retry] = 5,
+        **kwargs,
+    ) -> None:
+        super().__init__()
+        self.instance_url = instance_url
+        self._token = token
+        self.read_only = read_only
+
+        self.session = requests.session()
+
+        adapter = requests.adapters.HTTPAdapter(max_retries=max_retries)
+
+        self.insecure = insecure
+        if self.insecure:
+            self.session.mount("http://", adapter)
+        else:
+            self.session.mount("https://", adapter)
+
+        self.header = {"Authorization": "token " + self._token} if self._token else {}
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    def __str__(self) -> str:
+        token_str = (
+            f", token='{self._token[:1]}***{self._token[-1:]}'" if self._token else ""
+        )
+        insecure_str = ", insecure=True" if self.insecure else ""
+        readonly_str = ", read_only=True" if self.read_only else ""
+
+        return (
+            f"PagureService(instance_url='{self.instance_url}'"
+            f"{token_str}"
+            f"{readonly_str}"
+            f"{insecure_str})"
+        )
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, PagureService):
+            return False
+
+        return (
+            self._token == o._token  # type: ignore
+            and self.read_only == o.read_only  # type: ignore
+            and self.instance_url == o.instance_url  # type: ignore
+            and self.insecure == o.insecure  # type: ignore
+            and self.header == o.header  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(self, **kwargs) -> "PagureProject":
+        if "username" in kwargs:
+            return PagureProject(service=self, **kwargs)
+
+        return PagureProject(
+            service=self,
+            username=self.user.get_username(),
+            **kwargs,
+        )
+
+    def get_project_from_url(self, url: str) -> "PagureProject":
+        repo_url = parse_git_repo(potential_url=url)
+        if not repo_url:
+            raise OgrException(f"Cannot parse project url: '{url}'")
+
+        if not repo_url.is_fork:
+            repo_url.username = None
+
+        return self.get_project(
+            repo=repo_url.repo,
+            namespace=repo_url.namespace,
+            is_fork=repo_url.is_fork,
+            username=repo_url.username,
+        )
+
+    @property
+    def user(self) -> "PagureUser":
+        return PagureUser(service=self)
+
+    def call_api(
+        self,
+        url: str,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data=None,
+    ) -> dict:
+        """
+        Call API endpoint.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            Dictionary representing response.
+
+        Raises:
+            PagureAPIException, if error occurs.
+        """
+        response = self.call_api_raw(url=url, method=method, params=params, data=data)
+
+        if response.status_code == 404:
+            error_msg = (
+                response.json_content["error"]
+                if response.json_content and "error" in response.json_content
+                else None
+            )
+            raise PagureAPIException(
+                f"Page '{url}' not found when calling Pagure API.",
+                pagure_error=error_msg,
+                response_code=response.status_code,
+            )
+
+        if not response.json_content:
+            logger.debug(response.content)
+            raise PagureAPIException(
+                "Error while decoding JSON: {0}",
+                response_code=response.status_code,
+            )
+
+        if not response.ok:
+            logger.error(response.json_content)
+            if "error" in response.json_content:
+                error_msg = response.json_content["error"]
+                error_msg_ext = response.json_content.get("errors", "")
+                msg = f"Pagure API returned an error when calling '{url}': {error_msg}"
+                if error_msg_ext:
+                    msg += f" - {error_msg_ext}"
+                raise PagureAPIException(
+                    msg,
+                    pagure_error=error_msg,
+                    pagure_response=response.json_content,
+                    response_code=response.status_code,
+                )
+            raise PagureAPIException(
+                f"Problem with Pagure API when calling '{url}'",
+                response_code=response.status_code,
+            )
+
+        return response.json_content
+
+    def call_api_raw(
+        self,
+        url: str,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data=None,
+    ):
+        """
+        Call API endpoint and returns raw response.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            `RequestResponse` object that represents the response from the API
+            endpoint.
+        """
+
+        method = method or "GET"
+        try:
+            response = self.get_raw_request(
+                method=method,
+                url=url,
+                params=params,
+                data=data,
+            )
+
+        except requests.exceptions.ConnectionError as er:
+            logger.error(er)
+            raise OgrNetworkError(f"Cannot connect to url: '{url}'.") from er
+
+        if response.status_code >= 500:
+            raise GitForgeInternalError(
+                f"Pagure API returned {response.status_code} status for `{url}`"
+                f" with reason: `{response.reason}`",
+            )
+
+        return response
+
+    def get_raw_request(
+        self,
+        url,
+        method="GET",
+        params=None,
+        data=None,
+        header=None,
+    ) -> RequestResponse:
+        """
+        Call API endpoint and wrap the response in `RequestResponse` type.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+
+                Defaults to `"GET"`.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+            header: Header of the HTTP request.
+
+        Returns:
+            `RequestResponse` object representing the response.
+
+        Raises:
+            ValueError, if JSON cannot be retrieved.
+        """
+
+        response = self.session.request(
+            method=method,
+            url=url,
+            params=params,
+            headers=header or self.header,
+            data=data,
+            verify=not self.insecure,
+        )
+
+        json_output = None
+        try:
+            json_output = response.json()
+        except ValueError:
+            logger.debug(response.text)
+
+        return RequestResponse(
+            status_code=response.status_code,
+            ok=response.ok,
+            content=response.content,
+            json=json_output,
+            reason=response.reason,
+        )
+
+    @property
+    def api_url(self):
+        """URL to the Pagure API."""
+        return f"{self.instance_url}/api/0/"
+
+    def get_api_url(self, *args, add_api_endpoint_part: bool = True) -> str:
+        """
+        Get a URL from its parts.
+
+        Args:
+            *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+            add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+                Defaults to `True`.
+
+        Returns:
+            String
+        """
+        args_list: list[str] = []
+
+        args_list += filter(lambda x: x is not None, args)
+
+        if add_api_endpoint_part:
+            return self.api_url + "/".join(args_list)
+        return f"{self.instance_url}/" + "/".join(args_list)
+
+    def get_api_version(self) -> str:
+        """
+        Returns:
+            Version of the Pagure API.
+        """
+        request_url = self.get_api_url("version")
+        return_value = self.call_api(request_url)
+        return return_value["version"]
+
+    def get_error_codes(self):
+        """
+        Returns:
+            Dictionary with all error codes.
+        """
+        request_url = self.get_api_url("error_codes")
+        return self.call_api(request_url)
+
+    def change_token(self, token: str):
+        self._token = token
+        self.header = {"Authorization": "token " + self._token}
+
+    def __handle_project_create_fail(
+        self,
+        exception: PagureAPIException,
+        namespace: str,
+    ) -> None:
+        if (
+            exception.pagure_response
+            and exception.pagure_response["errors"]["namespace"][0]
+            == "Not a valid choice"
+        ):
+            request_url = self.get_api_url("group", namespace)
+
+            try:
+                self.call_api(request_url, data={"projects": False})
+            except PagureAPIException as ex:
+                raise OgrException(f"Namespace doesn't exist ({namespace}).") from ex
+
+            raise OgrException(
+                "Cannot create project in given namespace (permissions).",
+            )
+
+        raise exception
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> PagureProject:
+        request_url = self.get_api_url("new")
+
+        parameters = {"name": repo, "description": description, "wait": True}
+        if not description:
+            parameters["description"] = repo
+        if namespace:
+            parameters["namespace"] = namespace
+
+        try:
+            self.call_api(request_url, "POST", data=parameters)
+        except PagureAPIException as ex:
+            self.__handle_project_create_fail(ex, namespace)
+        return PagureProject(repo=repo, namespace=namespace, service=self)
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        raise OperationNotSupported
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class PagureService +(token: Optional[str] = None, instance_url: str = 'https://src.fedoraproject.org', read_only: bool = False, insecure: bool = False, max_retries: Union[int, urllib3.util.retry.Retry] = 5, **kwargs) +
+
+

Attributes

+
+
instance_url : str
+
URL of the git forge instance.
+
+
+ +Expand source code + +
@use_for_service("pagure")
+@use_for_service("src.fedoraproject.org")
+@use_for_service("src.stg.fedoraproject.org")
+@use_for_service("pkgs.fedoraproject.org")
+@use_for_service("pkgs.stg.fedoraproject.org")
+@use_for_service("git.centos.org")
+@use_for_service("git.stg.centos.org")
+class PagureService(BaseGitService):
+    def __init__(
+        self,
+        token: Optional[str] = None,
+        instance_url: str = "https://src.fedoraproject.org",
+        read_only: bool = False,
+        insecure: bool = False,
+        max_retries: Union[int, urllib3.util.Retry] = 5,
+        **kwargs,
+    ) -> None:
+        super().__init__()
+        self.instance_url = instance_url
+        self._token = token
+        self.read_only = read_only
+
+        self.session = requests.session()
+
+        adapter = requests.adapters.HTTPAdapter(max_retries=max_retries)
+
+        self.insecure = insecure
+        if self.insecure:
+            self.session.mount("http://", adapter)
+        else:
+            self.session.mount("https://", adapter)
+
+        self.header = {"Authorization": "token " + self._token} if self._token else {}
+
+        if kwargs:
+            logger.warning(f"Ignored keyword arguments: {kwargs}")
+
+    def __str__(self) -> str:
+        token_str = (
+            f", token='{self._token[:1]}***{self._token[-1:]}'" if self._token else ""
+        )
+        insecure_str = ", insecure=True" if self.insecure else ""
+        readonly_str = ", read_only=True" if self.read_only else ""
+
+        return (
+            f"PagureService(instance_url='{self.instance_url}'"
+            f"{token_str}"
+            f"{readonly_str}"
+            f"{insecure_str})"
+        )
+
+    def __eq__(self, o: object) -> bool:
+        if not issubclass(o.__class__, PagureService):
+            return False
+
+        return (
+            self._token == o._token  # type: ignore
+            and self.read_only == o.read_only  # type: ignore
+            and self.instance_url == o.instance_url  # type: ignore
+            and self.insecure == o.insecure  # type: ignore
+            and self.header == o.header  # type: ignore
+        )
+
+    def __hash__(self) -> int:
+        return hash(str(self))
+
+    def get_project(self, **kwargs) -> "PagureProject":
+        if "username" in kwargs:
+            return PagureProject(service=self, **kwargs)
+
+        return PagureProject(
+            service=self,
+            username=self.user.get_username(),
+            **kwargs,
+        )
+
+    def get_project_from_url(self, url: str) -> "PagureProject":
+        repo_url = parse_git_repo(potential_url=url)
+        if not repo_url:
+            raise OgrException(f"Cannot parse project url: '{url}'")
+
+        if not repo_url.is_fork:
+            repo_url.username = None
+
+        return self.get_project(
+            repo=repo_url.repo,
+            namespace=repo_url.namespace,
+            is_fork=repo_url.is_fork,
+            username=repo_url.username,
+        )
+
+    @property
+    def user(self) -> "PagureUser":
+        return PagureUser(service=self)
+
+    def call_api(
+        self,
+        url: str,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data=None,
+    ) -> dict:
+        """
+        Call API endpoint.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            Dictionary representing response.
+
+        Raises:
+            PagureAPIException, if error occurs.
+        """
+        response = self.call_api_raw(url=url, method=method, params=params, data=data)
+
+        if response.status_code == 404:
+            error_msg = (
+                response.json_content["error"]
+                if response.json_content and "error" in response.json_content
+                else None
+            )
+            raise PagureAPIException(
+                f"Page '{url}' not found when calling Pagure API.",
+                pagure_error=error_msg,
+                response_code=response.status_code,
+            )
+
+        if not response.json_content:
+            logger.debug(response.content)
+            raise PagureAPIException(
+                "Error while decoding JSON: {0}",
+                response_code=response.status_code,
+            )
+
+        if not response.ok:
+            logger.error(response.json_content)
+            if "error" in response.json_content:
+                error_msg = response.json_content["error"]
+                error_msg_ext = response.json_content.get("errors", "")
+                msg = f"Pagure API returned an error when calling '{url}': {error_msg}"
+                if error_msg_ext:
+                    msg += f" - {error_msg_ext}"
+                raise PagureAPIException(
+                    msg,
+                    pagure_error=error_msg,
+                    pagure_response=response.json_content,
+                    response_code=response.status_code,
+                )
+            raise PagureAPIException(
+                f"Problem with Pagure API when calling '{url}'",
+                response_code=response.status_code,
+            )
+
+        return response.json_content
+
+    def call_api_raw(
+        self,
+        url: str,
+        method: Optional[str] = None,
+        params: Optional[dict] = None,
+        data=None,
+    ):
+        """
+        Call API endpoint and returns raw response.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+
+        Returns:
+            `RequestResponse` object that represents the response from the API
+            endpoint.
+        """
+
+        method = method or "GET"
+        try:
+            response = self.get_raw_request(
+                method=method,
+                url=url,
+                params=params,
+                data=data,
+            )
+
+        except requests.exceptions.ConnectionError as er:
+            logger.error(er)
+            raise OgrNetworkError(f"Cannot connect to url: '{url}'.") from er
+
+        if response.status_code >= 500:
+            raise GitForgeInternalError(
+                f"Pagure API returned {response.status_code} status for `{url}`"
+                f" with reason: `{response.reason}`",
+            )
+
+        return response
+
+    def get_raw_request(
+        self,
+        url,
+        method="GET",
+        params=None,
+        data=None,
+        header=None,
+    ) -> RequestResponse:
+        """
+        Call API endpoint and wrap the response in `RequestResponse` type.
+
+        Args:
+            url: URL to be called.
+            method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+
+                Defaults to `"GET"`.
+            params: HTTP(S) query parameters in form of a dictionary.
+            data: Data to be sent in form of a dictionary.
+            header: Header of the HTTP request.
+
+        Returns:
+            `RequestResponse` object representing the response.
+
+        Raises:
+            ValueError, if JSON cannot be retrieved.
+        """
+
+        response = self.session.request(
+            method=method,
+            url=url,
+            params=params,
+            headers=header or self.header,
+            data=data,
+            verify=not self.insecure,
+        )
+
+        json_output = None
+        try:
+            json_output = response.json()
+        except ValueError:
+            logger.debug(response.text)
+
+        return RequestResponse(
+            status_code=response.status_code,
+            ok=response.ok,
+            content=response.content,
+            json=json_output,
+            reason=response.reason,
+        )
+
+    @property
+    def api_url(self):
+        """URL to the Pagure API."""
+        return f"{self.instance_url}/api/0/"
+
+    def get_api_url(self, *args, add_api_endpoint_part: bool = True) -> str:
+        """
+        Get a URL from its parts.
+
+        Args:
+            *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+            add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+                Defaults to `True`.
+
+        Returns:
+            String
+        """
+        args_list: list[str] = []
+
+        args_list += filter(lambda x: x is not None, args)
+
+        if add_api_endpoint_part:
+            return self.api_url + "/".join(args_list)
+        return f"{self.instance_url}/" + "/".join(args_list)
+
+    def get_api_version(self) -> str:
+        """
+        Returns:
+            Version of the Pagure API.
+        """
+        request_url = self.get_api_url("version")
+        return_value = self.call_api(request_url)
+        return return_value["version"]
+
+    def get_error_codes(self):
+        """
+        Returns:
+            Dictionary with all error codes.
+        """
+        request_url = self.get_api_url("error_codes")
+        return self.call_api(request_url)
+
+    def change_token(self, token: str):
+        self._token = token
+        self.header = {"Authorization": "token " + self._token}
+
+    def __handle_project_create_fail(
+        self,
+        exception: PagureAPIException,
+        namespace: str,
+    ) -> None:
+        if (
+            exception.pagure_response
+            and exception.pagure_response["errors"]["namespace"][0]
+            == "Not a valid choice"
+        ):
+            request_url = self.get_api_url("group", namespace)
+
+            try:
+                self.call_api(request_url, data={"projects": False})
+            except PagureAPIException as ex:
+                raise OgrException(f"Namespace doesn't exist ({namespace}).") from ex
+
+            raise OgrException(
+                "Cannot create project in given namespace (permissions).",
+            )
+
+        raise exception
+
+    def project_create(
+        self,
+        repo: str,
+        namespace: Optional[str] = None,
+        description: Optional[str] = None,
+    ) -> PagureProject:
+        request_url = self.get_api_url("new")
+
+        parameters = {"name": repo, "description": description, "wait": True}
+        if not description:
+            parameters["description"] = repo
+        if namespace:
+            parameters["namespace"] = namespace
+
+        try:
+            self.call_api(request_url, "POST", data=parameters)
+        except PagureAPIException as ex:
+            self.__handle_project_create_fail(ex, namespace)
+        return PagureProject(repo=repo, namespace=namespace, service=self)
+
+    def list_projects(
+        self,
+        namespace: Optional[str] = None,
+        user: Optional[str] = None,
+        search_pattern: Optional[str] = None,
+        language: Optional[str] = None,
+    ) -> list[GitProject]:
+        raise OperationNotSupported
+
+

Ancestors

+ +

Instance variables

+
+
var api_url
+
+

URL to the Pagure API.

+
+ +Expand source code + +
@property
+def api_url(self):
+    """URL to the Pagure API."""
+    return f"{self.instance_url}/api/0/"
+
+
+
+

Methods

+
+
+def call_api(self, url: str, method: Optional[str] = None, params: Optional[dict] = None, data=None) ‑> dict +
+
+

Call API endpoint.

+

Args

+
+
url
+
URL to be called.
+
method
+
Method of the HTTP request, e.g. "GET", "POST", etc.
+
params
+
HTTP(S) query parameters in form of a dictionary.
+
data
+
Data to be sent in form of a dictionary.
+
+

Returns

+

Dictionary representing response.

+

Raises

+

PagureAPIException, if error occurs.

+
+ +Expand source code + +
def call_api(
+    self,
+    url: str,
+    method: Optional[str] = None,
+    params: Optional[dict] = None,
+    data=None,
+) -> dict:
+    """
+    Call API endpoint.
+
+    Args:
+        url: URL to be called.
+        method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+        params: HTTP(S) query parameters in form of a dictionary.
+        data: Data to be sent in form of a dictionary.
+
+    Returns:
+        Dictionary representing response.
+
+    Raises:
+        PagureAPIException, if error occurs.
+    """
+    response = self.call_api_raw(url=url, method=method, params=params, data=data)
+
+    if response.status_code == 404:
+        error_msg = (
+            response.json_content["error"]
+            if response.json_content and "error" in response.json_content
+            else None
+        )
+        raise PagureAPIException(
+            f"Page '{url}' not found when calling Pagure API.",
+            pagure_error=error_msg,
+            response_code=response.status_code,
+        )
+
+    if not response.json_content:
+        logger.debug(response.content)
+        raise PagureAPIException(
+            "Error while decoding JSON: {0}",
+            response_code=response.status_code,
+        )
+
+    if not response.ok:
+        logger.error(response.json_content)
+        if "error" in response.json_content:
+            error_msg = response.json_content["error"]
+            error_msg_ext = response.json_content.get("errors", "")
+            msg = f"Pagure API returned an error when calling '{url}': {error_msg}"
+            if error_msg_ext:
+                msg += f" - {error_msg_ext}"
+            raise PagureAPIException(
+                msg,
+                pagure_error=error_msg,
+                pagure_response=response.json_content,
+                response_code=response.status_code,
+            )
+        raise PagureAPIException(
+            f"Problem with Pagure API when calling '{url}'",
+            response_code=response.status_code,
+        )
+
+    return response.json_content
+
+
+
+def call_api_raw(self, url: str, method: Optional[str] = None, params: Optional[dict] = None, data=None) +
+
+

Call API endpoint and returns raw response.

+

Args

+
+
url
+
URL to be called.
+
method
+
Method of the HTTP request, e.g. "GET", "POST", etc.
+
params
+
HTTP(S) query parameters in form of a dictionary.
+
data
+
Data to be sent in form of a dictionary.
+
+

Returns

+

RequestResponse object that represents the response from the API +endpoint.

+
+ +Expand source code + +
def call_api_raw(
+    self,
+    url: str,
+    method: Optional[str] = None,
+    params: Optional[dict] = None,
+    data=None,
+):
+    """
+    Call API endpoint and returns raw response.
+
+    Args:
+        url: URL to be called.
+        method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+        params: HTTP(S) query parameters in form of a dictionary.
+        data: Data to be sent in form of a dictionary.
+
+    Returns:
+        `RequestResponse` object that represents the response from the API
+        endpoint.
+    """
+
+    method = method or "GET"
+    try:
+        response = self.get_raw_request(
+            method=method,
+            url=url,
+            params=params,
+            data=data,
+        )
+
+    except requests.exceptions.ConnectionError as er:
+        logger.error(er)
+        raise OgrNetworkError(f"Cannot connect to url: '{url}'.") from er
+
+    if response.status_code >= 500:
+        raise GitForgeInternalError(
+            f"Pagure API returned {response.status_code} status for `{url}`"
+            f" with reason: `{response.reason}`",
+        )
+
+    return response
+
+
+
+def get_api_url(self, *args, add_api_endpoint_part: bool = True) ‑> str +
+
+

Get a URL from its parts.

+

Args

+
+
*args
+
String parts of the URL, e.g. "a", "b" will call project/a/b
+
add_api_endpoint_part
+
+

Add part with API endpoint (/api/0/).

+

Defaults to True.

+
+
+

Returns

+

String

+
+ +Expand source code + +
def get_api_url(self, *args, add_api_endpoint_part: bool = True) -> str:
+    """
+    Get a URL from its parts.
+
+    Args:
+        *args: String parts of the URL, e.g. `"a", "b"` will call `project/a/b`
+        add_api_endpoint_part: Add part with API endpoint (`/api/0/`).
+
+            Defaults to `True`.
+
+    Returns:
+        String
+    """
+    args_list: list[str] = []
+
+    args_list += filter(lambda x: x is not None, args)
+
+    if add_api_endpoint_part:
+        return self.api_url + "/".join(args_list)
+    return f"{self.instance_url}/" + "/".join(args_list)
+
+
+
+def get_api_version(self) ‑> str +
+
+

Returns

+

Version of the Pagure API.

+
+ +Expand source code + +
def get_api_version(self) -> str:
+    """
+    Returns:
+        Version of the Pagure API.
+    """
+    request_url = self.get_api_url("version")
+    return_value = self.call_api(request_url)
+    return return_value["version"]
+
+
+
+def get_error_codes(self) +
+
+

Returns

+

Dictionary with all error codes.

+
+ +Expand source code + +
def get_error_codes(self):
+    """
+    Returns:
+        Dictionary with all error codes.
+    """
+    request_url = self.get_api_url("error_codes")
+    return self.call_api(request_url)
+
+
+
+def get_raw_request(self, url, method='GET', params=None, data=None, header=None) ‑> RequestResponse +
+
+

Call API endpoint and wrap the response in RequestResponse type.

+

Args

+
+
url
+
URL to be called.
+
method
+
+

Method of the HTTP request, e.g. "GET", "POST", etc.

+

Defaults to "GET".

+
+
params
+
HTTP(S) query parameters in form of a dictionary.
+
data
+
Data to be sent in form of a dictionary.
+
header
+
Header of the HTTP request.
+
+

Returns

+

RequestResponse object representing the response.

+

Raises

+

ValueError, if JSON cannot be retrieved.

+
+ +Expand source code + +
def get_raw_request(
+    self,
+    url,
+    method="GET",
+    params=None,
+    data=None,
+    header=None,
+) -> RequestResponse:
+    """
+    Call API endpoint and wrap the response in `RequestResponse` type.
+
+    Args:
+        url: URL to be called.
+        method: Method of the HTTP request, e.g. `"GET"`, `"POST"`, etc.
+
+            Defaults to `"GET"`.
+        params: HTTP(S) query parameters in form of a dictionary.
+        data: Data to be sent in form of a dictionary.
+        header: Header of the HTTP request.
+
+    Returns:
+        `RequestResponse` object representing the response.
+
+    Raises:
+        ValueError, if JSON cannot be retrieved.
+    """
+
+    response = self.session.request(
+        method=method,
+        url=url,
+        params=params,
+        headers=header or self.header,
+        data=data,
+        verify=not self.insecure,
+    )
+
+    json_output = None
+    try:
+        json_output = response.json()
+    except ValueError:
+        logger.debug(response.text)
+
+    return RequestResponse(
+        status_code=response.status_code,
+        ok=response.ok,
+        content=response.content,
+        json=json_output,
+        reason=response.reason,
+    )
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/services/pagure/user.html b/docs/services/pagure/user.html new file mode 100644 index 00000000..baca6e62 --- /dev/null +++ b/docs/services/pagure/user.html @@ -0,0 +1,211 @@ + + + + + + +ogr.services.pagure.user API documentation + + + + + + + + + + + +
+
+
+

Module ogr.services.pagure.user

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+
+from ogr.exceptions import OperationNotSupported
+from ogr.services import pagure as ogr_pagure
+from ogr.services.base import BaseGitUser
+from ogr.services.pagure.project import PagureProject
+
+
+class PagureUser(BaseGitUser):
+    service: "ogr_pagure.PagureService"
+
+    def __init__(self, service: "ogr_pagure.PagureService") -> None:
+        super().__init__(service=service)
+
+    def __str__(self) -> str:
+        return f'PagureUser(username="{self.get_username()}")'
+
+    def get_username(self) -> str:
+        request_url = self.service.get_api_url("-", "whoami")
+
+        return_value = self.service.call_api(url=request_url, method="POST", data={})
+        return return_value["username"]
+
+    def get_projects(self) -> list["PagureProject"]:
+        user_url = self.service.get_api_url("user", self.get_username())
+        raw_projects = self.service.call_api(user_url)["repos"]
+
+        return [
+            PagureProject(
+                repo=project["name"],
+                namespace=project["namespace"],
+                service=self.service,
+            )
+            for project in raw_projects
+        ]
+
+    def get_forks(self) -> list["PagureProject"]:
+        user_url = self.service.get_api_url("user", self.get_username())
+        raw_forks = self.service.call_api(user_url)["forks"]
+
+        return [
+            PagureProject(
+                repo=fork["name"],
+                namespace=fork["namespace"],
+                service=self.service,
+                is_fork=True,
+            )
+            for fork in raw_forks
+        ]
+
+    def get_email(self) -> str:
+        # Not supported by Pagure
+        raise OperationNotSupported(
+            "Pagure does not support retrieving of user's email address",
+        )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class PagureUser +(service: ogr_pagure.PagureService) +
+
+

Represents currently authenticated user through service.

+
+ +Expand source code + +
class PagureUser(BaseGitUser):
+    service: "ogr_pagure.PagureService"
+
+    def __init__(self, service: "ogr_pagure.PagureService") -> None:
+        super().__init__(service=service)
+
+    def __str__(self) -> str:
+        return f'PagureUser(username="{self.get_username()}")'
+
+    def get_username(self) -> str:
+        request_url = self.service.get_api_url("-", "whoami")
+
+        return_value = self.service.call_api(url=request_url, method="POST", data={})
+        return return_value["username"]
+
+    def get_projects(self) -> list["PagureProject"]:
+        user_url = self.service.get_api_url("user", self.get_username())
+        raw_projects = self.service.call_api(user_url)["repos"]
+
+        return [
+            PagureProject(
+                repo=project["name"],
+                namespace=project["namespace"],
+                service=self.service,
+            )
+            for project in raw_projects
+        ]
+
+    def get_forks(self) -> list["PagureProject"]:
+        user_url = self.service.get_api_url("user", self.get_username())
+        raw_forks = self.service.call_api(user_url)["forks"]
+
+        return [
+            PagureProject(
+                repo=fork["name"],
+                namespace=fork["namespace"],
+                service=self.service,
+                is_fork=True,
+            )
+            for fork in raw_forks
+        ]
+
+    def get_email(self) -> str:
+        # Not supported by Pagure
+        raise OperationNotSupported(
+            "Pagure does not support retrieving of user's email address",
+        )
+
+

Ancestors

+ +

Class variables

+
+
var servicePagureService
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/utils.html b/docs/utils.html new file mode 100644 index 00000000..ba44db62 --- /dev/null +++ b/docs/utils.html @@ -0,0 +1,642 @@ + + + + + + +ogr.utils API documentation + + + + + + + + + + + +
+
+
+

Module ogr.utils

+
+
+
+ +Expand source code + +
# Copyright Contributors to the Packit project.
+# SPDX-License-Identifier: MIT
+
+import functools
+import logging
+import re
+from re import Match
+from typing import Any, Callable, Optional, Union
+
+from ogr.abstract import AnyComment, Comment
+
+logger = logging.getLogger(__name__)
+
+
+def filter_comments(
+    comments: list[AnyComment],
+    filter_regex: Optional[str] = None,
+    reverse: bool = False,
+    author: Optional[str] = None,
+) -> list[AnyComment]:
+    """
+    Filters comments from the given list.
+
+    Args:
+        comments: List of comments to be filtered.
+        filter_regex: Regex to be used for filtering body of the
+            comments.
+
+            Defaults to `None`, which means no filtering by regex.
+        reverse: Specifies ordering of the comments.
+
+            Defaults to `False`, which means the order is kept from the input.
+        author: Login of the author of the comments.
+
+            Defaults to `None`, which means no filtering by author.
+
+    Returns:
+        List of comments that satisfy requested criteria.
+    """
+    if reverse:
+        comments.reverse()
+
+    if filter_regex or author:
+        pattern = None
+        if filter_regex:
+            pattern = re.compile(filter_regex)
+
+        comments = list(
+            filter(
+                lambda comment: (not pattern or bool(pattern.search(comment.body)))
+                and (not author or comment.author == author),
+                comments,
+            ),
+        )
+    return comments
+
+
+def search_in_comments(
+    comments: list[Union[str, Comment]],
+    filter_regex: str,
+) -> Optional[Match[str]]:
+    """
+    Find match in pull request description or comments.
+
+    Args:
+        comments: List of comments or bodies of comments
+            to be searched through.
+        filter_regex: Regex to be used for filtering with `re.search`.
+
+    Returns:
+        Match that has been found, `None` otherwise.
+    """
+    pattern = re.compile(filter_regex)
+    for comment in comments:
+        if isinstance(comment, Comment):
+            comment = comment.body
+        re_search = pattern.search(comment)
+        if re_search:
+            return re_search
+    return None
+
+
+class RequestResponse:
+    """
+    Class that holds response for Pagure requests.
+
+    Attributes:
+        status_code (int): Status code of the response.
+        ok (bool): `True` if successful, `False` otherwise.
+        content (bytes): Content of the response.
+        json_content (Optional[Dict[Any, Any]]): JSON content of the response.
+    """
+
+    def __init__(
+        self,
+        status_code: int,
+        ok: bool,
+        content: bytes,
+        json: Optional[dict[Any, Any]] = None,
+        reason: Optional[str] = None,
+        headers: Optional[list[tuple[Any, Any]]] = None,
+        links: Optional[list[str]] = None,
+        exception: Optional[dict[Any, Any]] = None,
+    ) -> None:
+        self.status_code = status_code
+        self.ok = ok
+        self.content = content
+        self.json_content = json
+        self.reason = reason
+        self.headers = dict(headers) if headers else None
+        self.links = links
+        self.exception = exception
+
+    def __str__(self) -> str:
+        return (
+            f"RequestResponse("
+            f"status_code={self.status_code}, "
+            f"ok={self.ok}, "
+            f"content={self.content.decode()}, "
+            f"json={self.json_content}, "
+            f"reason={self.reason}, "
+            f"headers={self.headers}, "
+            f"links={self.links}, "
+            f"exception={self.exception})"
+        )
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, RequestResponse):
+            return False
+        return (
+            self.status_code == o.status_code
+            and self.ok == o.ok
+            and self.content == o.content
+            and self.json_content == o.json_content
+            and self.reason == o.reason
+            and self.headers == o.headers
+            and self.links == o.links
+            and self.exception == o.exception
+        )
+
+    def to_json_format(self) -> dict[str, Any]:
+        """
+        Returns:
+            Response in a JSON format.
+        """
+        output = {
+            "status_code": self.status_code,
+            "ok": self.ok,
+            "content": self.content,
+        }
+        if self.json_content:
+            output["json"] = self.json_content
+        if self.reason:
+            output["reason"] = self.reason
+        if self.headers:
+            output["headers"] = self.headers
+        if self.links:
+            output["links"] = self.links
+        if self.exception:
+            output["exception"] = self.exception
+        return output
+
+    def json(self) -> Optional[dict[Any, Any]]:
+        """
+        Returns:
+            JSON content of the response.
+        """
+        return self.json_content
+
+
+def filter_paths(paths: list[str], filter_regex: str) -> list[str]:
+    """
+    Filters paths from the given list.
+
+    Args:
+        paths: List of paths to be filtered.
+        filter_regex: Regex to be used for filtering paths.
+
+    Returns:
+        List of path that satisfy regex.
+    """
+    pattern = re.compile(filter_regex)
+    return [path for path in paths if (not pattern or bool(pattern.search(path)))]
+
+
+def indirect(specialized_function: Callable) -> Any:
+    """
+    Decorator to wrap methods on `GitProject`s that call specialized classes.
+
+    Args:
+        specialized_function: Static method of the specialized class
+            that takes as first argument the `GitProject` itself.
+
+    Returns:
+        Decorator that calls `specialized_function` once called.
+    """
+
+    def indirect_caller(func):
+        @functools.wraps(func)
+        def indirectly_called(self, *args, **kwargs):
+            return specialized_function(self, *args, **kwargs)
+
+        return indirectly_called
+
+    return indirect_caller
+
+
+
+
+
+
+
+

Functions

+
+
+def filter_comments(comments: list[~AnyComment], filter_regex: Optional[str] = None, reverse: bool = False, author: Optional[str] = None) ‑> list[~AnyComment] +
+
+

Filters comments from the given list.

+

Args

+
+
comments
+
List of comments to be filtered.
+
filter_regex
+
+

Regex to be used for filtering body of the +comments.

+

Defaults to None, which means no filtering by regex.

+
+
reverse
+
+

Specifies ordering of the comments.

+

Defaults to False, which means the order is kept from the input.

+
+
author
+
+

Login of the author of the comments.

+

Defaults to None, which means no filtering by author.

+
+
+

Returns

+

List of comments that satisfy requested criteria.

+
+ +Expand source code + +
def filter_comments(
+    comments: list[AnyComment],
+    filter_regex: Optional[str] = None,
+    reverse: bool = False,
+    author: Optional[str] = None,
+) -> list[AnyComment]:
+    """
+    Filters comments from the given list.
+
+    Args:
+        comments: List of comments to be filtered.
+        filter_regex: Regex to be used for filtering body of the
+            comments.
+
+            Defaults to `None`, which means no filtering by regex.
+        reverse: Specifies ordering of the comments.
+
+            Defaults to `False`, which means the order is kept from the input.
+        author: Login of the author of the comments.
+
+            Defaults to `None`, which means no filtering by author.
+
+    Returns:
+        List of comments that satisfy requested criteria.
+    """
+    if reverse:
+        comments.reverse()
+
+    if filter_regex or author:
+        pattern = None
+        if filter_regex:
+            pattern = re.compile(filter_regex)
+
+        comments = list(
+            filter(
+                lambda comment: (not pattern or bool(pattern.search(comment.body)))
+                and (not author or comment.author == author),
+                comments,
+            ),
+        )
+    return comments
+
+
+
+def filter_paths(paths: list[str], filter_regex: str) ‑> list[str] +
+
+

Filters paths from the given list.

+

Args

+
+
paths
+
List of paths to be filtered.
+
filter_regex
+
Regex to be used for filtering paths.
+
+

Returns

+

List of path that satisfy regex.

+
+ +Expand source code + +
def filter_paths(paths: list[str], filter_regex: str) -> list[str]:
+    """
+    Filters paths from the given list.
+
+    Args:
+        paths: List of paths to be filtered.
+        filter_regex: Regex to be used for filtering paths.
+
+    Returns:
+        List of path that satisfy regex.
+    """
+    pattern = re.compile(filter_regex)
+    return [path for path in paths if (not pattern or bool(pattern.search(path)))]
+
+
+
+def indirect(specialized_function: Callable) ‑> Any +
+
+

Decorator to wrap methods on GitProjects that call specialized classes.

+

Args

+
+
specialized_function
+
Static method of the specialized class +that takes as first argument the GitProject itself.
+
+

Returns

+

Decorator that calls specialized_function once called.

+
+ +Expand source code + +
def indirect(specialized_function: Callable) -> Any:
+    """
+    Decorator to wrap methods on `GitProject`s that call specialized classes.
+
+    Args:
+        specialized_function: Static method of the specialized class
+            that takes as first argument the `GitProject` itself.
+
+    Returns:
+        Decorator that calls `specialized_function` once called.
+    """
+
+    def indirect_caller(func):
+        @functools.wraps(func)
+        def indirectly_called(self, *args, **kwargs):
+            return specialized_function(self, *args, **kwargs)
+
+        return indirectly_called
+
+    return indirect_caller
+
+
+
+def search_in_comments(comments: list[typing.Union[str, Comment]], filter_regex: str) ‑> Optional[re.Match[str]] +
+
+

Find match in pull request description or comments.

+

Args

+
+
comments
+
List of comments or bodies of comments +to be searched through.
+
filter_regex
+
Regex to be used for filtering with re.search.
+
+

Returns

+

Match that has been found, None otherwise.

+
+ +Expand source code + +
def search_in_comments(
+    comments: list[Union[str, Comment]],
+    filter_regex: str,
+) -> Optional[Match[str]]:
+    """
+    Find match in pull request description or comments.
+
+    Args:
+        comments: List of comments or bodies of comments
+            to be searched through.
+        filter_regex: Regex to be used for filtering with `re.search`.
+
+    Returns:
+        Match that has been found, `None` otherwise.
+    """
+    pattern = re.compile(filter_regex)
+    for comment in comments:
+        if isinstance(comment, Comment):
+            comment = comment.body
+        re_search = pattern.search(comment)
+        if re_search:
+            return re_search
+    return None
+
+
+
+
+
+

Classes

+
+
+class RequestResponse +(status_code: int, ok: bool, content: bytes, json: Optional[dict[typing.Any, typing.Any]] = None, reason: Optional[str] = None, headers: Optional[list[tuple[typing.Any, typing.Any]]] = None, links: Optional[list[str]] = None, exception: Optional[dict[typing.Any, typing.Any]] = None) +
+
+

Class that holds response for Pagure requests.

+

Attributes

+
+
status_code : int
+
Status code of the response.
+
ok : bool
+
True if successful, False otherwise.
+
content : bytes
+
Content of the response.
+
json_content : Optional[Dict[Any, Any]]
+
JSON content of the response.
+
+
+ +Expand source code + +
class RequestResponse:
+    """
+    Class that holds response for Pagure requests.
+
+    Attributes:
+        status_code (int): Status code of the response.
+        ok (bool): `True` if successful, `False` otherwise.
+        content (bytes): Content of the response.
+        json_content (Optional[Dict[Any, Any]]): JSON content of the response.
+    """
+
+    def __init__(
+        self,
+        status_code: int,
+        ok: bool,
+        content: bytes,
+        json: Optional[dict[Any, Any]] = None,
+        reason: Optional[str] = None,
+        headers: Optional[list[tuple[Any, Any]]] = None,
+        links: Optional[list[str]] = None,
+        exception: Optional[dict[Any, Any]] = None,
+    ) -> None:
+        self.status_code = status_code
+        self.ok = ok
+        self.content = content
+        self.json_content = json
+        self.reason = reason
+        self.headers = dict(headers) if headers else None
+        self.links = links
+        self.exception = exception
+
+    def __str__(self) -> str:
+        return (
+            f"RequestResponse("
+            f"status_code={self.status_code}, "
+            f"ok={self.ok}, "
+            f"content={self.content.decode()}, "
+            f"json={self.json_content}, "
+            f"reason={self.reason}, "
+            f"headers={self.headers}, "
+            f"links={self.links}, "
+            f"exception={self.exception})"
+        )
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, RequestResponse):
+            return False
+        return (
+            self.status_code == o.status_code
+            and self.ok == o.ok
+            and self.content == o.content
+            and self.json_content == o.json_content
+            and self.reason == o.reason
+            and self.headers == o.headers
+            and self.links == o.links
+            and self.exception == o.exception
+        )
+
+    def to_json_format(self) -> dict[str, Any]:
+        """
+        Returns:
+            Response in a JSON format.
+        """
+        output = {
+            "status_code": self.status_code,
+            "ok": self.ok,
+            "content": self.content,
+        }
+        if self.json_content:
+            output["json"] = self.json_content
+        if self.reason:
+            output["reason"] = self.reason
+        if self.headers:
+            output["headers"] = self.headers
+        if self.links:
+            output["links"] = self.links
+        if self.exception:
+            output["exception"] = self.exception
+        return output
+
+    def json(self) -> Optional[dict[Any, Any]]:
+        """
+        Returns:
+            JSON content of the response.
+        """
+        return self.json_content
+
+

Methods

+
+
+def json(self) ‑> Optional[dict[typing.Any, typing.Any]] +
+
+

Returns

+

JSON content of the response.

+
+ +Expand source code + +
def json(self) -> Optional[dict[Any, Any]]:
+    """
+    Returns:
+        JSON content of the response.
+    """
+    return self.json_content
+
+
+
+def to_json_format(self) ‑> dict[str, typing.Any] +
+
+

Returns

+

Response in a JSON format.

+
+ +Expand source code + +
def to_json_format(self) -> dict[str, Any]:
+    """
+    Returns:
+        Response in a JSON format.
+    """
+    output = {
+        "status_code": self.status_code,
+        "ok": self.ok,
+        "content": self.content,
+    }
+    if self.json_content:
+        output["json"] = self.json_content
+    if self.reason:
+        output["reason"] = self.reason
+    if self.headers:
+        output["headers"] = self.headers
+    if self.links:
+        output["links"] = self.links
+    if self.exception:
+        output["exception"] = self.exception
+    return output
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file