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,
    PRLabel,
    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
from ogr.services.pagure.label import PagurePRLabel

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

    @property
    def labels(self) -> list[PRLabel]:
        return [PagurePRLabel(label, self) for label in self._raw_pr["tags"]]

    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

    @property
    def labels(self) -> list[PRLabel]:
        return [PagurePRLabel(label, self) for label in self._raw_pr["tags"]]

    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