Source code for action0.github.operations.pulls
"""The pull request operations (`GitHub docs <https://docs.github.com/en/rest/pulls/pulls>`__)."""
from __future__ import annotations
from enum import StrEnum
from typing import Any
from action0.client import json_field
from action0.client import path_param
from action0.client import query
from action0.req import Method
from ..models.commit import Commit
from ..models.commit import CommitFile
from ..models.issue import IssueState
from ..models.pull import MergeResult
from ..models.pull import PullRequest
from .base import GitHubOperation
from .base import PaginatedOperation
from .base import SortDirection
[docs]
class PullStateFilter(StrEnum):
"""The state filter of :py:class:`ListPulls` (unlike
:py:class:`~action0.github.models.issue.IssueState` it knows ``all``)."""
OPEN = "open"
CLOSED = "closed"
ALL = "all"
[docs]
class PullSort(StrEnum):
"""The sort orders of the pull request listing."""
CREATED = "created"
UPDATED = "updated"
POPULARITY = "popularity"
LONG_RUNNING = "long-running"
[docs]
class MergeMethod(StrEnum):
"""How :py:class:`MergePull` merges."""
MERGE = "merge"
SQUASH = "squash"
REBASE = "rebase"
[docs]
class ListPulls(PaginatedOperation[PullRequest]):
"""
``GET /repos/{owner}/{repo}/pulls`` — list a repository's pull
requests.
>>> operation = ListPulls(owner="python", repo="peps", state=PullStateFilter.CLOSED)
>>> operation.as_request("https://api.github.com").url.as_str()
'https://api.github.com/repos/python/peps/pulls?per_page=30&page=1&state=closed'
"""
method = Method.GET
path = "/repos/{owner}/{repo}/pulls"
owner: str = path_param()
repo: str = path_param()
state: PullStateFilter | None = query(default=None)
"""The state filter; ``None`` uses GitHub's default (``open``)."""
head: str | None = query(default=None)
"""Only pull requests from this head, as ``"owner:branch"``
(e.g. ``"octocat:new-topic"``)."""
base: str | None = query(default=None)
"""Only pull requests targeting this base branch name
(e.g. ``"main"``)."""
sort: PullSort | None = query(default=None)
"""The sort order; ``None`` uses GitHub's default (``created``)."""
direction: SortDirection | None = query(default=None)
"""The sort direction; ``None`` uses GitHub's default (``desc``
when sorting by ``created``, ``asc`` otherwise)."""
[docs]
def load_item(self, data: Any) -> PullRequest:
"""
:param data: one decoded JSON array item
:return: the pull request
"""
return PullRequest.from_json(data)
[docs]
class GetPull(GitHubOperation[PullRequest]):
"""
``GET /repos/{owner}/{repo}/pulls/{pull_number}`` — fetch one pull
request, including the merge/diff statistics the listings omit
(:py:attr:`~action0.github.models.pull.PullRequest.mergeable`,
:py:attr:`~action0.github.models.pull.PullRequest.commits`, …).
>>> operation = GetPull(owner="python", repo="peps", pull_number=42)
>>> operation.as_request("https://api.github.com").url.as_str()
'https://api.github.com/repos/python/peps/pulls/42'
"""
method = Method.GET
path = "/repos/{owner}/{repo}/pulls/{pull_number}"
owner: str = path_param()
repo: str = path_param()
pull_number: int = path_param()
[docs]
def load_json(self, data: Any) -> PullRequest:
"""
:param data: the decoded JSON payload
:return: the pull request
"""
return PullRequest.from_json(data)
[docs]
class CreatePull(GitHubOperation[PullRequest]):
"""
``POST /repos/{owner}/{repo}/pulls`` — open a pull request.
The non-path fields become the JSON request body; ``None`` fields
are omitted from it (requires a token with write access to the
repository).
"""
method = Method.POST
path = "/repos/{owner}/{repo}/pulls"
owner: str = path_param()
repo: str = path_param()
title: str = json_field()
"""The pull request title."""
head: str = json_field()
"""The branch with the changes — a plain branch name, or
``"owner:branch"`` for a cross-repository (fork) pull request."""
base: str = json_field()
"""The branch the changes should be merged into (in the
``{owner}/{repo}`` repository), e.g. ``"main"``."""
body: str | None = json_field(default=None)
"""The description text (GitHub-flavored Markdown)."""
draft: bool | None = json_field(default=None)
"""Open as a draft pull request; ``None`` uses GitHub's default
(``False``)."""
[docs]
def load_json(self, data: Any) -> PullRequest:
"""
:param data: the decoded JSON payload
:return: the created pull request (with its server-assigned
number)
"""
return PullRequest.from_json(data)
[docs]
class UpdatePull(GitHubOperation[PullRequest]):
"""
``PATCH /repos/{owner}/{repo}/pulls/{pull_number}`` — update a pull
request. PATCH semantics as in
:py:class:`~action0.github.operations.issues.UpdateIssue`: a
``None`` field is omitted from the body and leaves the pull request
untouched. Note "merged" is not a state —
:py:class:`MergePull` merges, ``state`` only opens/closes.
"""
method = Method.PATCH
path = "/repos/{owner}/{repo}/pulls/{pull_number}"
owner: str = path_param()
repo: str = path_param()
pull_number: int = path_param()
title: str | None = json_field(default=None)
"""The new title; ``None`` leaves it unchanged."""
body: str | None = json_field(default=None)
"""The new description text; ``None`` leaves it unchanged."""
state: IssueState | None = json_field(default=None)
"""Close or reopen the pull request; ``None`` leaves the state
unchanged."""
base: str | None = json_field(default=None)
"""Retarget to this base branch name; ``None`` leaves it
unchanged."""
[docs]
def load_json(self, data: Any) -> PullRequest:
"""
:param data: the decoded JSON payload
:return: the updated pull request
"""
return PullRequest.from_json(data)
[docs]
class MergePull(GitHubOperation[MergeResult]):
"""
``PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge`` — merge a
pull request.
An unmergeable pull request (conflicts, failing required checks, a
``sha`` guard mismatch) is answered with 405/409, surfacing as an
:py:class:`~action0.client.errors.APIError`. Although PUT is
nominally idempotent, re-merging an already merged pull request
also 405s — the retry policy's method gate is no license here, so
prefer the ``sha`` guard for defensive merging.
"""
method = Method.PUT
path = "/repos/{owner}/{repo}/pulls/{pull_number}/merge"
owner: str = path_param()
repo: str = path_param()
pull_number: int = path_param()
merge_method: MergeMethod | None = json_field(default=None)
"""How to merge; ``None`` uses the repository's default method."""
commit_title: str | None = json_field(default=None)
"""The merge commit title; ``None`` uses GitHub's default."""
commit_message: str | None = json_field(default=None)
"""The merge commit message body; ``None`` uses GitHub's default."""
sha: str | None = json_field(default=None)
"""Only merge if the head is still at this sha — guards against
merging commits pushed after the last review."""
[docs]
def load_json(self, data: Any) -> MergeResult:
"""
:param data: the decoded JSON payload
:return: the merge result
"""
return MergeResult.from_json(data)
[docs]
class ListPullFiles(PaginatedOperation[CommitFile]):
"""
``GET /repos/{owner}/{repo}/pulls/{pull_number}/files`` — list the
files a pull request changes (the same per-file diff shape commits
use; GitHub caps the listing at 3000 files).
>>> operation = ListPullFiles(owner="python", repo="peps", pull_number=42)
>>> operation.as_request("https://api.github.com").url.as_str()
'https://api.github.com/repos/python/peps/pulls/42/files?per_page=30&page=1'
"""
method = Method.GET
path = "/repos/{owner}/{repo}/pulls/{pull_number}/files"
owner: str = path_param()
repo: str = path_param()
pull_number: int = path_param()
[docs]
def load_item(self, data: Any) -> CommitFile:
"""
:param data: one decoded JSON array item
:return: the diff file
"""
return CommitFile.from_json(data)
[docs]
class ListPullCommits(PaginatedOperation[Commit]):
"""
``GET /repos/{owner}/{repo}/pulls/{pull_number}/commits`` — list a
pull request's commits, oldest first (capped at 250 by GitHub; for
more, use :py:class:`~action0.github.operations.commits.ListCommits`
on the head branch).
>>> operation = ListPullCommits(owner="python", repo="peps", pull_number=42)
>>> operation.as_request("https://api.github.com").url.as_str()
'https://api.github.com/repos/python/peps/pulls/42/commits?per_page=30&page=1'
"""
method = Method.GET
path = "/repos/{owner}/{repo}/pulls/{pull_number}/commits"
owner: str = path_param()
repo: str = path_param()
pull_number: int = path_param()
[docs]
def load_item(self, data: Any) -> Commit:
"""
:param data: one decoded JSON array item
:return: the commit
"""
return Commit.from_json(data)