Source code for action0.github.operations.issues
"""The issue operations (`GitHub docs <https://docs.github.com/en/rest/issues/issues>`__)."""
from __future__ import annotations
from datetime import datetime
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.comment import IssueComment
from ..models.issue import Issue
from ..models.issue import IssueState
from .base import GitHubOperation
from .base import NoContentOperation
from .base import PaginatedOperation
from .base import SortDirection
[docs]
class IssueStateFilter(StrEnum):
"""The state filter of :py:class:`ListIssues` (unlike
:py:class:`~action0.github.models.issue.IssueState` it knows ``all``)."""
OPEN = "open"
CLOSED = "closed"
ALL = "all"
[docs]
class IssueSort(StrEnum):
"""The sort orders of the issue listing."""
CREATED = "created"
UPDATED = "updated"
COMMENTS = "comments"
[docs]
class IssueStateReason(StrEnum):
"""The reason attached to an issue's state by :py:class:`UpdateIssue`."""
COMPLETED = "completed"
NOT_PLANNED = "not_planned"
REOPENED = "reopened"
[docs]
class LockReason(StrEnum):
"""The reason :py:class:`LockIssue` attaches to a locked
conversation (note ``"too heated"`` — GitHub's value contains a
space)."""
OFF_TOPIC = "off-topic"
TOO_HEATED = "too heated"
RESOLVED = "resolved"
SPAM = "spam"
[docs]
class ListIssues(PaginatedOperation[Issue]):
"""
``GET /repos/{owner}/{repo}/issues`` — list a repository's issues.
GitHub returns pull requests here too (every pull request is an
issue); filter them out via
:py:attr:`~action0.github.models.issue.Issue.is_pull_request`.
>>> operation = ListIssues(owner="python", repo="peps", state=IssueStateFilter.CLOSED)
>>> operation.as_request("https://api.github.com").url.as_str()
'https://api.github.com/repos/python/peps/issues?per_page=30&page=1&state=closed'
"""
method = Method.GET
path = "/repos/{owner}/{repo}/issues"
owner: str = path_param()
repo: str = path_param()
state: IssueStateFilter | None = query(default=None)
"""The state filter; ``None`` uses GitHub's default (``open``)."""
labels: str | None = query(default=None)
"""Label names to filter by, comma-separated (``"bug,ui"``) — GitHub's
own wire format for this parameter."""
sort: IssueSort | 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``)."""
since: datetime | None = query(default=None)
"""Only issues updated at or after this time (serialized to ISO 8601)."""
[docs]
def load_item(self, data: Any) -> Issue:
"""
:param data: one decoded JSON array item
:return: the issue
"""
return Issue.from_json(data)
[docs]
class GetIssue(GitHubOperation[Issue]):
"""
``GET /repos/{owner}/{repo}/issues/{issue_number}`` — fetch one
issue (or pull request — see
:py:attr:`~action0.github.models.issue.Issue.is_pull_request`).
>>> operation = GetIssue(owner="python", repo="peps", issue_number=42)
>>> operation.as_request("https://api.github.com").url.as_str()
'https://api.github.com/repos/python/peps/issues/42'
"""
method = Method.GET
path = "/repos/{owner}/{repo}/issues/{issue_number}"
owner: str = path_param()
repo: str = path_param()
issue_number: int = path_param()
[docs]
def load_json(self, data: Any) -> Issue:
"""
:param data: the decoded JSON payload
:return: the issue
"""
return Issue.from_json(data)
[docs]
class CreateIssue(GitHubOperation[Issue]):
"""
``POST /repos/{owner}/{repo}/issues`` — create an issue.
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}/issues"
owner: str = path_param()
repo: str = path_param()
title: str = json_field()
"""The issue title."""
body: str | None = json_field(default=None)
"""The description text (GitHub-flavored Markdown)."""
labels: list[str] | None = json_field(default=None)
"""Label names to attach."""
assignees: list[str] | None = json_field(default=None)
"""Logins to assign."""
[docs]
def load_json(self, data: Any) -> Issue:
"""
:param data: the decoded JSON payload
:return: the created issue (with its server-assigned number)
"""
return Issue.from_json(data)
[docs]
class UpdateIssue(GitHubOperation[Issue]):
"""
``PATCH /repos/{owner}/{repo}/issues/{issue_number}`` — update an
issue (requires a token with write access to the repository).
PATCH semantics: only the fields you set are changed — a ``None``
field is omitted from the JSON body and leaves the issue untouched.
(This also means clearing a field by sending JSON ``null`` is not
expressible here; send an empty string/list instead where GitHub
accepts one.) Like every non-idempotent method, a PATCH is never
blindly repeated by
:py:class:`~action0.github.retry.GitHubRetryPolicy`.
"""
method = Method.PATCH
path = "/repos/{owner}/{repo}/issues/{issue_number}"
owner: str = path_param()
repo: str = path_param()
issue_number: int = path_param()
title: str | None = json_field(default=None)
"""The new title."""
body: str | None = json_field(default=None)
"""The new description text (GitHub-flavored Markdown)."""
state: IssueState | None = json_field(default=None)
"""Open or close the issue."""
state_reason: IssueStateReason | None = json_field(default=None)
"""The reason to attach to the state change (close as
``completed``/``not_planned``, reopen as ``reopened``)."""
labels: list[str] | None = json_field(default=None)
"""The new label names — replaces the whole set (``[]`` clears it)."""
assignees: list[str] | None = json_field(default=None)
"""The new assignee logins — replaces the whole set (``[]`` clears
it)."""
[docs]
def load_json(self, data: Any) -> Issue:
"""
:param data: the decoded JSON payload
:return: the updated issue
"""
return Issue.from_json(data)
[docs]
class LockIssue(NoContentOperation):
"""
``PUT /repos/{owner}/{repo}/issues/{issue_number}/lock`` — lock an
issue's (or pull request's) conversation: only collaborators can
comment until it is unlocked. Answers ``204``.
"""
method = Method.PUT
path = "/repos/{owner}/{repo}/issues/{issue_number}/lock"
owner: str = path_param()
repo: str = path_param()
issue_number: int = path_param()
lock_reason: LockReason | None = json_field(default=None)
"""The reason shown in the timeline; ``None`` locks without one."""
[docs]
class UnlockIssue(NoContentOperation):
"""
``DELETE /repos/{owner}/{repo}/issues/{issue_number}/lock`` —
unlock the conversation again. Answers ``204``.
"""
method = Method.DELETE
path = "/repos/{owner}/{repo}/issues/{issue_number}/lock"
owner: str = path_param()
repo: str = path_param()
issue_number: int = path_param()
[docs]
class AddAssignees(GitHubOperation[Issue]):
"""
``POST /repos/{owner}/{repo}/issues/{issue_number}/assignees`` —
add assignees to an issue or pull request, keeping the existing
ones (unlike :py:class:`UpdateIssue`'s ``assignees``, which
replaces the whole set). Unassignable logins are silently ignored
by GitHub.
"""
method = Method.POST
path = "/repos/{owner}/{repo}/issues/{issue_number}/assignees"
owner: str = path_param()
repo: str = path_param()
issue_number: int = path_param()
assignees: list[str] = json_field()
"""The logins to add (at most 10 assignees in total)."""
[docs]
def load_json(self, data: Any) -> Issue:
"""
:param data: the decoded JSON payload
:return: the issue with its updated assignee set
"""
return Issue.from_json(data)
[docs]
class RemoveAssignees(GitHubOperation[Issue]):
"""
``DELETE /repos/{owner}/{repo}/issues/{issue_number}/assignees`` —
remove assignees from an issue or pull request. A DELETE carrying a
JSON body — GitHub's design, unusual but valid HTTP; the fields
serialize exactly like every other body.
"""
method = Method.DELETE
path = "/repos/{owner}/{repo}/issues/{issue_number}/assignees"
owner: str = path_param()
repo: str = path_param()
issue_number: int = path_param()
assignees: list[str] = json_field()
"""The logins to remove (others stay assigned)."""
[docs]
def load_json(self, data: Any) -> Issue:
"""
:param data: the decoded JSON payload
:return: the issue with its updated assignee set
"""
return Issue.from_json(data)