Source code for action0.github.operations.base
"""
The base classes shared by all GitHub operations
(:py:class:`GitHubOperation`, :py:class:`PaginatedOperation`,
:py:class:`NoContentOperation`) and the query vocabularies GitHub uses
across resource areas (:py:class:`SortDirection`).
"""
from abc import abstractmethod
from dataclasses import replace
from enum import StrEnum
from typing import Any
from typing import TypeVar
from action0.client import JsonOperation
from action0.client import Operation
from action0.client import query
from action0.req import Response
from ..models.page import ItemT
from ..models.page import Page
from .links import links
R_co = TypeVar("R_co", covariant=True)
"""The parsed result type of a GitHub operation."""
PageT = TypeVar("PageT", bound=Page[Any])
"""A page result — :py:class:`~action0.github.models.page.Page` or a
subclass like :py:class:`~action0.github.models.search.SearchPage`."""
[docs]
def attach_next(operation: Any, page: PageT, response: Response) -> PageT:
"""
Attach the next-page operation to a freshly parsed page: a copy of
the given operation with its ``page`` field incremented — exactly
when the response's ``Link`` header announces a ``rel="next"``
(GitHub's authoritative end-of-listing signal).
``dataclasses.replace`` keeps the page's concrete type, so subclasses
like :py:class:`~action0.github.models.search.SearchPage` pass
through with their extra fields intact.
:param operation: the operation that produced the page — any
operation dataclass with a ``page`` field (typed
``Any``: "has a page field" spans the unrelated
:py:class:`PaginatedOperation` and
:py:class:`~action0.github.operations.search.SearchOperation`
hierarchies)
:param page: the parsed page, ``next`` not yet set
:param response: the response it was parsed from
:return: the page, with ``next`` attached if there is one
"""
if "next" in links(response):
return replace(page, next=replace(operation, page=operation.page + 1))
return page
[docs]
class SortDirection(StrEnum):
"""The sort direction of a listing."""
ASC = "asc"
DESC = "desc"
[docs]
class GitHubOperation(JsonOperation[R_co]):
"""
The base class of all GitHub operations: a
:py:class:`~action0.client.operation.JsonOperation` requesting GitHub's
recommended media type.
``accept`` lives here (not only as a client default header) because
:py:meth:`~action0.client.operation.Operation.as_request` sets the
operation's ``Accept`` before the client's gap-filling defaults run —
``JsonOperation``'s plain ``application/json`` would win otherwise.
"""
accept = "application/vnd.github+json"
[docs]
class NoContentOperation(Operation[None]):
"""
The base class of the operations whose success answer is ``204 No
Content`` — deletes, locks and the like. There is nothing to parse,
so ``send`` yields ``None`` (wrapped in whatever the execution model
wraps results in); errors surface as usual via
:py:class:`~action0.client.errors.APIError`.
Not a :py:class:`GitHubOperation`: ``JsonOperation``'s ``load``
treats an empty body as an error, which is exactly the success case
here.
"""
accept = "application/vnd.github+json"
[docs]
def load(self, response: Response) -> None:
"""
Nothing to parse — a vetted response *is* the success.
:param response: the response, already vetted
:return: ``None``, always
"""
return None
[docs]
class PaginatedOperation(GitHubOperation[Page[ItemT]]):
"""
The base class of the listing operations: GitHub's page-number
pagination as query fields, and the result wrapped as a
:py:class:`~action0.github.models.page.Page` whose ``next`` is the
ready-to-send operation for the following page — present exactly when
the response's ``Link`` header announces a ``rel="next"`` (GitHub's
authoritative signal), built as a copy of this operation with
:py:attr:`page` + 1.
Subclasses implement :py:meth:`load_item` for a single JSON array
item. Being base-class fields, ``per_page`` and ``page`` come first
in every listing's query string.
"""
per_page: int = query(default=30)
"""The page size (GitHub caps it at 100)."""
page: int = query(default=1)
"""The page number, starting at 1."""
[docs]
@abstractmethod
def load_item(self, data: Any) -> ItemT:
"""
Turn one item of the decoded JSON array into the typed model.
:param data: one decoded JSON array item
:return: the parsed item
"""
[docs]
def load_json(self, data: Any) -> Page[ItemT]:
"""
:param data: the decoded JSON payload (an array)
:return: the page, without pagination yet (:py:meth:`load` adds
it — only the response's ``Link`` header knows)
"""
return Page(items=[self.load_item(item) for item in data])
[docs]
def load(self, response: Response) -> Page[ItemT]:
"""
Decode the page (via :py:class:`JsonOperation
<action0.client.operation.JsonOperation>`'s JSON handling) and
attach the next-page operation if the response's ``Link`` header
announces one.
:param response: the response, already vetted
:return: the page
"""
return attach_next(self, super().load(response), response)