Source code for action0.github.operations.contents
"""The repository content operations
(`GitHub docs <https://docs.github.com/en/rest/repos/contents>`__)."""
from __future__ import annotations
import base64
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.content import ContentFile
from ..models.content import DirectoryEntry
from ..models.content import FileCommit
from .base import GitHubOperation
def _base64_str(data: bytes) -> str:
"""
Encode raw file bytes the way the contents API wants them in the
JSON body.
:param data: the raw bytes
:return: the base64 text
"""
return base64.b64encode(data).decode("ascii")
[docs]
class GetContent(GitHubOperation[ContentFile | list[DirectoryEntry]]):
"""
``GET /repos/{owner}/{repo}/contents/{file_path}`` — fetch a file or
list a directory.
GitHub answers with an *object* for a file (base64-encoded content
inlined) and an *array* for a directory — the result type is the
union, dispatched on the payload shape:
>>> operation = GetContent(owner="octo", repo="demo", file_path="src/app.py")
>>> operation.as_request("https://api.github.com").url.as_str()
'https://api.github.com/repos/octo/demo/contents/src/app.py'
(The field is ``file_path`` because ``path`` is the operation's own
path template attribute; an empty string lists the repository
root.)
"""
method = Method.GET
path = "/repos/{owner}/{repo}/contents/{file_path}"
owner: str = path_param()
repo: str = path_param()
file_path: str = path_param()
"""The path within the repository — a file for a
:py:class:`~action0.github.models.content.ContentFile` result, a
directory (or ``""`` for the root) for a list of
:py:class:`~action0.github.models.content.DirectoryEntry`."""
ref: str | None = query(default=None)
"""The branch, tag or sha to read from; ``None`` uses the
repository's default branch."""
[docs]
def load_json(self, data: Any) -> ContentFile | list[DirectoryEntry]:
"""
:param data: the decoded JSON payload — object or array
:return: the file, or the directory listing
"""
if isinstance(data, list):
return [DirectoryEntry.from_json(item) for item in data]
return ContentFile.from_json(data)
[docs]
class GetReadme(GitHubOperation[ContentFile]):
"""
``GET /repos/{owner}/{repo}/readme`` — fetch a repository's README,
whatever it is called (``README.md``, ``README.rst``, …).
>>> operation = GetReadme(owner="python", repo="peps")
>>> operation.as_request("https://api.github.com").url.as_str()
'https://api.github.com/repos/python/peps/readme'
"""
method = Method.GET
path = "/repos/{owner}/{repo}/readme"
owner: str = path_param()
repo: str = path_param()
ref: str | None = query(default=None)
"""The branch, tag or sha to read from; ``None`` uses the
repository's default branch."""
[docs]
def load_json(self, data: Any) -> ContentFile:
"""
:param data: the decoded JSON payload
:return: the README file
"""
return ContentFile.from_json(data)
[docs]
class CreateOrUpdateFile(GitHubOperation[FileCommit]):
"""
``PUT /repos/{owner}/{repo}/contents/{file_path}`` — create a file,
or update one (requires a token with write access; every call is
one commit).
Create and update are the same endpoint, told apart by :py:attr:`sha`:
``None`` creates — GitHub answers 422 if the file already exists —
and passing the file's current blob sha updates, answering 409 on a
mismatch (someone else wrote in between; both surface as
:py:class:`~action0.client.errors.APIError`). Pass raw bytes as
:py:attr:`content` — the base64 transport encoding is applied on
serialization (a ``serialize=`` field hook).
"""
method = Method.PUT
path = "/repos/{owner}/{repo}/contents/{file_path}"
owner: str = path_param()
repo: str = path_param()
file_path: str = path_param()
"""The path of the file within the repository."""
message: str = json_field()
"""The commit message."""
content: bytes = json_field(serialize=_base64_str, repr=False)
"""The new file content, raw — base64 happens on the wire."""
sha: str | None = json_field(default=None)
"""The blob sha the file currently has
(:py:attr:`~action0.github.models.content.ContentFile.sha`) when
updating; ``None`` creates a new file."""
branch: str | None = json_field(default=None)
"""The branch to commit to; ``None`` uses the repository's default
branch."""
[docs]
def load_json(self, data: Any) -> FileCommit:
"""
:param data: the decoded JSON payload
:return: the created commit and the written file (whose fresh
``sha`` the next update of the same file needs)
"""
return FileCommit.from_json(data)
[docs]
class DeleteFile(GitHubOperation[FileCommit]):
"""
``DELETE /repos/{owner}/{repo}/contents/{file_path}`` — delete a
file, as one commit. Unusually for a DELETE it carries a JSON body
(the commit message and the blob sha) *and* answers with one — the
commit — so this is no
:py:class:`~action0.github.operations.base.NoContentOperation`.
"""
method = Method.DELETE
path = "/repos/{owner}/{repo}/contents/{file_path}"
owner: str = path_param()
repo: str = path_param()
file_path: str = path_param()
"""The path of the file within the repository."""
message: str = json_field()
"""The commit message."""
sha: str = json_field()
"""The blob sha the file currently has — required; GitHub answers
409 on a mismatch."""
branch: str | None = json_field(default=None)
"""The branch to commit to; ``None`` uses the repository's default
branch."""
[docs]
def load_json(self, data: Any) -> FileCommit:
"""
:param data: the decoded JSON payload
:return: the deleting commit (``content`` is ``None``)
"""
return FileCommit.from_json(data)