Skip to main content

Action0-GitHub-API

CI PyPI

A fully typed GitHub REST API client built on action0-client: GitHub endpoints are described once, as typed operation dataclasses, and run synchronously, on asyncio or on Twisted — decided by the backend you plug in. The type checker follows along: the same send() returns a value, an Awaitable or a Deferred, depending on the backend.

client = GitHubClient(RequestsBackend())
repo = client.send(GetRepo(owner="python", repo="cpython"))  # Repo

client = GitHubClient(AsyncHttpxBackend())
repo = await client.send(GetRepo(owner="python", repo="cpython"))  # Awaitable[Repo]

client = GitHubClient(TwistedBackend())
deferred = client.send(GetRepo(owner="python", repo="cpython"))  # Deferred[Repo]

Installation

Pick the extra matching the HTTP backend you want to run on (the stdlib urllib and thread-pool backends of action0-client need no extra):

uv add "action0-github-api[httpx]"     # or [requests], [aiohttp], [urllib3], [twisted], [all]

Usage

from action0.client.backends.requests import RequestsBackend
from action0.github import CompareCommits, CreateIssue, CreateOrUpdateFile
from action0.github import DownloadReleaseAsset, GetCombinedStatus, GetLatestRelease
from action0.github import GetRateLimit, GetReadme, GetRepo, GetUser, GitHubClient, IssueState
from action0.github import IssueStateReason, ListCommits, ListOrgRepos, ListPulls, MergeMethod
from action0.github import MergePull, PullStateFilter, RepoSearchSort, RepoSort, SearchRepos
from action0.github import UpdateIssue, all_items

with RequestsBackend() as backend:
    client = GitHubClient(backend)  # token="ghp_..." for higher rate limits
    repo = client.send(GetRepo(owner="python", repo="cpython"))
    print(repo.full_name, repo.language, repo.stargazers_count)

    repos = client.send(ListOrgRepos(org="python", sort=RepoSort.PUSHED, per_page=10))
    print([r.name for r in repos])  # one Page[Repo]; repos.next is the next page's operation

    for repo in all_items(client, ListOrgRepos(org="python")):  # or follow all pages lazily
        print(repo.full_name)

    pulls = client.send(ListPulls(owner="python", repo="peps", state=PullStateFilter.OPEN))
    print([p.title for p in pulls if not p.draft])  # p.head/p.base name the branches

    commits = client.send(ListCommits(owner="python", repo="peps", file_path="pep-0008.txt"))
    print([c.message.splitlines()[0] for c in commits])  # newest first

    diff = client.send(CompareCommits(owner="python", repo="peps", base="main", head="topic"))
    print(diff.status, diff.ahead_by, [f.filename for f in diff.files])

    readme = client.send(GetReadme(owner="python", repo="peps"))
    print(readme.text[:40])  # contents arrive base64-encoded — .text decodes

    user = client.send(GetUser(username="gvanrossum"))
    print(user.name, user.followers)

    hits = client.send(SearchRepos(q="http client language:python", sort=RepoSearchSort.STARS))
    print(hits.total_count, [r.full_name for r in hits])

    limits = client.send(GetRateLimit())  # this call doesn't count against any limit
    print(limits.core.remaining, limits.search.remaining)

Writing works the same way — the typed fields become the JSON body (needs a token). Updates are PATCH semantics: None fields are omitted and stay untouched:

issue = client.send(CreateIssue(owner="octo", repo="demo", title="Found a bug", labels=["bug"]))
issue = client.send(
    UpdateIssue(
        owner="octo",
        repo="demo",
        issue_number=issue.number,
        state=IssueState.CLOSED,
        state_reason=IssueStateReason.COMPLETED,
    )
)

status = client.send(GetCombinedStatus(owner="octo", repo="demo", ref="main"))
result = client.send(  # merge once the statuses and checks are green
    MergePull(owner="octo", repo="demo", pull_number=42, merge_method=MergeMethod.SQUASH)
)

written = client.send(  # committing a file is one call — raw bytes in, base64 on the wire
    CreateOrUpdateFile(
        owner="octo", repo="demo", file_path="docs/note.md", message="Add note", content=b"# Hi\n"
    )
)

Release assets download as a stream — on a stream=True backend the body is never held in memory (GitHub 302-redirects to its CDN, so the backend must follow redirects; most do by default):

with RequestsBackend(stream=True) as backend:  # a second backend, just for downloads
    release = client.send(GetLatestRelease(owner="octo", repo="demo"))
    asset = release.assets[0]
    producer = GitHubClient(backend).send(
        DownloadReleaseAsset(owner="octo", repo="demo", asset_id=asset.id)
    )
    with open(asset.name, "wb") as file:
        for chunk in producer.chunks():
            file.write(chunk)

Uploading works too — UploadReleaseAsset streams a file to a release, sent through a client pointed at GITHUB_UPLOADS_URL (GitHub takes uploads on a separate host).

Rate limits are handled by wrapping the backend with action0-client's retrying wrapper and the GitHub-tuned policy — and saved in the first place by the conditional-requests hook, which revalidates via ETag (GitHub's 304 answers don't count against the rate limit):

backend = RetryingSyncBackend(
    RequestsBackend(hooks=[ConditionalRequestsHook()]),
    GitHubRetryPolicy(),
)
client = GitHubClient(backend, token="ghp_...")

See the quickstart for asyncio/Twisted usage, authentication, GitHub Enterprise Server and testing without network — and examples/get_repo.py for a complete, runnable example.

Status

The core resource areas are covered: repositories (contents incl. file writes, branches, tags, topics, languages, contributors, collaborators), issues (comments, labels, milestones, assignees — each fully manageable), pull requests (merging, reviews incl. line comments, review requests), commits (incl. statuses and check runs), releases (CRUD, generated notes, streaming asset down- and uploads), users, organizations, search and rate limits. Documentation: https://laughinjar.github.io/action0-github-api/

AI disclosure

This library is developed with heavy use of AI coding tools: the code, tests, and documentation are largely written by Claude Code, working from the author's design brief and reviewed by the author. If that changes how much you want to rely on this package, that's a fair call — read the source, it's small.

License

MIT — see LICENSE.

Metadata

Release files for action0-github-api 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for action0-github-api 0.1.0
File Size Uploaded
action0_github_api-0.1.0.tar.gz 236.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for action0-github-api 0.1.0
File Interpreter ABI Platform
action0_github_api-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 310.4 kB

Release files / action0_github_api-0.1.0.tar.gz

Download URL action0_github_api-0.1.0.tar.gz
Size 236.2 kB
Tags Source
SHA-256 checksum
How to use checksums
02e1aa2b01ba9aa95cf3db45af178483916407505b3a3039c8b8b477b3700fb3
BLAKE2b-256 checksum
How to use checksums
247cba75102dbb43c3d855119c10a4ba6f4b206ed8ce73a85e12f505d292c5c5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.

Transparency log

Release files / action0_github_api-0.1.0-py3-none-any.whl

Download URL action0_github_api-0.1.0-py3-none-any.whl
Size 74.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2eba5c5aad3841705366590ca30f854bb4a45df711823897664e4e6f3e61663a
BLAKE2b-256 checksum
How to use checksums
8cdd8229932782b31e668d9d5eaab607ca98f2c6b12f9ec525a8313dc61f5b3c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page