Skip to main content

GitHub SDK for Python

Project description

githubkit

license pypi python black pyright ruff pre-commit

✨ The modern, all-batteries-included GitHub SDK for Python ✨

✨ Support both sync and async calls, fully typed

✨ Always up to date, like octokit ✨

Installation

pip install githubkit
# or, use poetry
poetry add githubkit
# or, use pdm
pdm add githubkit
# or, use uv
uv add githubkit

if you want to auth as github app, extra dependencies are required:

pip install githubkit[auth-app]
# or, use poetry
poetry add githubkit[auth-app]
# or, use pdm
pdm add githubkit[auth-app]
# or, use uv
uv add githubkit[auth-app]

if you want to mix sync and async calls in oauth device callback, extra dependencies are required:

pip install githubkit[auth-oauth-device]
# or, use poetry
poetry add githubkit[auth-oauth-device]
# or, use pdm
pdm add githubkit[auth-oauth-device]
# or, use uv
uv add githubkit[auth-oauth-device]

githubkit supports both pydantic v1 and v2, but pydantic v2 is recommended. If you have encountered any problems with pydantic v1/v2, please file an issue.

[!WARNING] githubkit uses GitHub's official openapi schema to generate apis and models. You may occasionally encounter breaking changes like model names or model field types changing when upgrading githubkit. This is due to upstream schema changes and githubkit can not control this.

githubkit recommends using a python dependency manager (like poetry / pdm / uv) to lock the version of githubkit to avoid unexpected changes.

Quick Start

Here is some common use cases to help you get started quickly. The following examples are written in sync style, you can also use async style by using functions with async_ prefix. For more detailed usage, please refer to the Usage section.

APIs are fully typed. Type hints in the following examples are just for reference only.

Use personal access token (PAT) to call GitHub API

from githubkit import GitHub
from githubkit.versions.latest.models import PublicUser, PrivateUser

github = GitHub("<your_token_here>")

# call GitHub rest api
resp = github.rest.users.get_authenticated()
user: PublicUser | PrivateUser = resp.parsed_data

# call GitHub graphql api
data: dict = github.graphql("{ viewer { login } }")

Develop an OAuth APP (GitHub APP) with web flow

OAuth web flow allows you to authenticate as a user and act on behalf of the user.

Note that if you are developing a GitHub APP, you may opt-in / opt-out of the user-to-server token expiration feature. If you opt-in, the user-to-server token will expire after a certain period of time, and you need to use the refresh token to generate a new token. In this case, you need to do more work to handle the token refresh. See GitHub Docs - Refreshing user access tokens for more information.

If you are developing an OAuth APP or a GitHub APP without user-to-server token expiration:

from githubkit.versions.latest.models import PublicUser, PrivateUser
from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy

github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))

# redirect user to github oauth page and get the code from callback

# one time usage
user_github = github.with_auth(github.auth.as_web_user("<code>"))

# or, store the user token in a database for later use
auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").exchange_token(github)
# store the user token to database
access_token = auth.token

# restore the user token from database

user_github = github.with_auth(
    OAuthTokenAuthStrategy(
        "<client_id>", "<client_secret>", token=access_token
    )
)

# now you can act as the user
resp = user_github.rest.users.get_authenticated()
user: PublicUser | PrivateUser = resp.parsed_data

# you can get the user name and id now
username = user.login
user_id = user.id

If you are developing a GitHub APP with user-to-server token expiration:

from githubkit.versions.latest.models import PublicUser, PrivateUser
from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy

github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))

# redirect user to github oauth page and get the code from callback

# one time usage
user_github = github.with_auth(github.auth.as_web_user("<code>"))

# or, store the user refresh token in a database for later use
auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").exchange_token(github)
refresh_token = auth.refresh_token

# restore the user refresh token from database

# you can use the refresh_token to generate a new token
auth = OAuthTokenAuthStrategy(
    "<client_id>", "<client_secret>", refresh_token=refresh_token
)
# refresh the token manually if you want to store the new refresh token
# otherwise, the token will be refreshed automatically when you make a request
auth.refresh(github)
refresh_token = auth.refresh_token

user_github = github.with_auth(auth)

# now you can act as the user
resp = user_github.rest.users.get_authenticated()
user: PublicUser | PrivateUser = resp.parsed_data

# you can get the user name and id now
username = user.login
user_id = user.id

Develop an OAuth APP (GitHub APP) with device flow

from githubkit import GitHub, OAuthDeviceAuthStrategy, OAuthTokenAuthStrategy

# sync/async func for displaying user code to user
def callback(data: dict):
    print(data["user_code"])

user_github = GitHub(OAuthDeviceAuthStrategy("<client_id>", callback))

# if you want to store the user token in a database
auth: OAuthTokenAuthStrategy = user_github.auth.exchange_token(user_github)
access_token = auth.token
refresh_token = auth.refresh_token
# restore the user token from database
user_github = user_github.with_auth(
    OAuthTokenAuthStrategy(
        "<client_id>", None, refresh_token=refresh_token
    )
)

Develop a GitHub APP

Authenticating as a installation by repository name:

from githubkit import GitHub, AppAuthStrategy
from githubkit.versions.latest.models import Issue, Installation

github = GitHub(
    AppAuthStrategy("your_app_id", "your_private_key", "client_id", "client_secret")
)

resp = github.rest.apps.get_repo_installation("owner", "repo")
repo_installation: Installation = resp.parsed_data

installation_github = github.with_auth(
    github.auth.as_installation(repo_installation.id)
)

# create a comment on an issue
resp = installation_github.rest.issues.create_comment("owner", "repo", 1, body="Hello")
issue: IssueComment = resp.parsed_data

Authenticating as a installation by username:

from githubkit import GitHub, AppAuthStrategy
from githubkit.versions.latest.models import Installation, IssueComment

github = GitHub(
    AppAuthStrategy("your_app_id", "your_private_key", "client_id", "client_secret")
)

resp = github.rest.apps.get_user_installation("username")
user_installation: Installation = resp.parsed_data

installation_github = github.with_auth(
    github.auth.as_installation(user_installation.id)
)

# create a comment on an issue
resp = installation_github.rest.issues.create_comment("owner", "repo", 1, body="Hello")
issue: IssueComment = resp.parsed_data

Usage

Authentication

Initialize a github client with no authentication:

from githubkit import GitHub, UnauthAuthStrategy

github = GitHub()
# or, use UnauthAuthStrategy
github = GitHub(UnauthAuthStrategy())

or using PAT (Token):

from githubkit import GitHub, TokenAuthStrategy

github = GitHub("<your_token_here>")
# or, use TokenAuthStrategy
github = GitHub(TokenAuthStrategy("<your_token_here>"))

or using GitHub APP authentication:

from githubkit import GitHub, AppAuthStrategy

github = GitHub(
    AppAuthStrategy(
        "<app_id>", "<private_key>", "<optional_client_id>", "<optional_client_secret>"
    )
)

or using GitHub APP Installation authentication:

from githubkit import GitHub, AppInstallationAuthStrategy

github = GitHub(
    AppInstallationAuthStrategy(
        "<app_id>", "<private_key>", installation_id, "<optional_client_id>", "<optional_client_secret>",
    )
)

or using OAuth APP authentication:

from githubkit import GitHub, OAuthAppAuthStrategy

github = GitHub(OAuthAppAuthStrategy("<client_id_here>", "<client_secret_here>"))

or using GitHub APP / OAuth APP token authentication (This is usefull when you stored the user token in a database):

from githubkit import GitHub, OAuthTokenAuthStrategy

github = GitHub(
    OAuthTokenAuthStrategy(
        "<client_id_here>",
        "<client_secret_here>",
        "<access_token_here>",
        "<access_token_expire_time_here>",
        "<refresh_token_here>",
        "<refresh_token_expire_time_here>",
    )
)

or using GitHub APP / OAuth APP web flow authentication:

from githubkit import GitHub, OAuthWebAuthStrategy

github = GitHub(
    OAuthWebAuthStrategy(
        "<client_id_here>", "<client_secret_here>", "<web_flow_exchange_code_here>"
    )
)

or using GitHub APP / OAuth APP device flow authentication:

from githubkit import GitHub, OAuthDeviceAuthStrategy

# sync/async func for displaying user code to user
def callback(data: dict):
  print(data["user_code"])

github = GitHub(
    OAuthDeviceAuthStrategy(
        "<client_id_here>", callback
    )
)

See Switch between AuthStrategy for more detail about oauth flow.

or using GitHub Action authentication:

from githubkit import GitHub, ActionAuthStrategy

github = GitHub(ActionAuthStrategy())

and add env or input to the step:

- name: Some step use githubkit
  with:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Some step use githubkit
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Config

githubkit is highly configurable, you can change the default config by passing config options to GitHub:

from githubkit import GitHub

github = GitHub(
    base_url="https://api.github.com/",
    accept_format="full+json",
    previews=["starfox"],
    user_agent="GitHubKit/Python",
    follow_redirects=True,
    timeout=None,
    http_cache=True,
    auto_retry=True,
)

The base_url option is used to set the base URL of the GitHub API. If you are using GitHub Enterprise Server, you need to include the /api/v3 path in the base URL.

The accept_format and previews are used to set the default Accept header, you can find more details in GitHub API docs.

The http_cache option enables the http caching feature powered by Hishel for HTTPX. GitHub API limits the number of requests that you can make within a specific amount of time. This feature is useful to reduce the number of requests to GitHub API and avoid hitting the rate limit.

The auto_retry option enables request retrying when rate limit exceeded and server error encountered. See Auto Retry for more infomation.

Calling Rest API

APIs are fully typed. Type hints in the following examples are just for reference only.

Simple sync call:

from githubkit import Response
from githubkit.versions.latest.models import FullRepository

resp: Response[FullRepository] = github.rest.repos.get(owner="owner", repo="repo")
repo: FullRepository = resp.parsed_data

Simple async call:

from githubkit import Response
from githubkit.versions.latest.models import FullRepository

resp: Response[FullRepository] = await github.rest.repos.async_get(owner="owner", repo="repo")
repo: FullRepository = resp.parsed_data

Call API with context (reusing client):

from githubkit import Response
from githubkit.versions.latest.models import FullRepository

with GitHub("<your_token_here>") as github:
    resp: Response[FullRepository] = github.rest.repos.get(owner="owner", repo="repo")
    repo: FullRepository = resp.parsed_data
from githubkit import Response
from githubkit.versions.latest.models import FullRepository

async with GitHub("<your_token_here>") as github:
    resp: Response[FullRepository] = await github.rest.repos.async_get(owner="owner", repo="repo")
    repo: FullRepository = resp.parsed_data

[!WARNING] Note that you should hold a strong reference to the githubkit client instance. Otherwise, githubkit client will fail to call the request. For example, you should not do this:

from githubkit import GitHub

def get_client() -> GitHub:
    return GitHub()

# This will cause error
get_client().rest.repos.get("owner", "repo")

# This is ok
client = get_client()
client.rest.repos.get("owner", "repo")

Data Validation

As shown above, the response data is parsed and validated by accessing the response.parsed_data property. This ensures that the data type returned by the API is as expected and your code is safe to use it (with static type checking). But sometimes you may want to get the raw data returned by the API, such as when the schema is not correct. You can use the response.text property or response.json() method to get the raw data:

from typing import Any, Dict
from githubkit import Response

resp: Response[FullRepository] = github.rest.repos.get(owner="owner", repo="repo")
repo: Dict[str, Any] = resp.json()

Rest API Versioning

APIs are fully typed. Different versions of APIs are typed separately.

githubkit supports all versions of GitHub API, you can switch between versions as follows:

github.rest("2022-11-28").repos.get(owner="owner", repo="repo")

The models of versions can be imported from githubkit.versions.<version>.models, for example:

from githubkit.versions.v2022_11_28.models import FullRepository

Specially, the latest version is always linked to the latest version of GitHub API:

from githubkit.versions.latest.models import FullRepository

[!NOTE] For backward compatibility, the githubkit.rest module is linked to the models of latest version by default.

from githubkit.rest import FullRepository

You can also get the latest version name of GitHub API and all versions mapping of GitHub API:

from githubkit.versions import LATEST_VERSION, VERSIONS

Current supported versions are: (you can find it in the section [[tool.codegen.descriptions]] of the pyproject.toml file)

  • 2022-11-28 (latest)
  • ghec-2022-11-28

Rest API Pagination

Pagination type checking is also supported:

Typing is tested with Pylance (Pyright).

from githubkit.versions.latest.models import Issue

for issue in github.paginate(
    github.rest.issues.list_for_repo, owner="owner", repo="repo", state="open"
):
    issue: Issue
    print(issue.number)
from githubkit.versions.latest.models import Issue

async for issue in github.paginate(
    github.rest.issues.async_list_for_repo, owner="owner", repo="repo", state="open"
):
    issue: Issue
    print(issue.number)

complex pagination with custom map function (some api returns data in a nested field):

async for accessible_repo in github.paginate(
    github.rest.apps.async_list_installation_repos_for_authenticated_user,
    map_func=lambda r: r.parsed_data.repositories,
    installation_id=1,
):
    accessible_repo: Repository
    print(accessible_repo.full_name)

Calling GraphQL API

Simple sync call:

data: Dict[str, Any] = github.graphql(query, variables={"foo": "bar"})

Simple async call:

data: Dict[str, Any] = await github.async_graphql(query, variables={"foo": "bar"})

GraphQL Pagination

githubkit also provides a helper function to paginate the GraphQL API.

First, You must accept a cursor parameter and return a pageInfo object in your query. For example:

query ($owner: String!, $repo: String!, $cursor: String) {
  repository(owner: $owner, name: $repo) {
    issues(first: 10, after: $cursor) {
      nodes {
        number
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}

The pageInfo object in your query must be one of the following types depending on the direction of the pagination:

For forward pagination, use:

pageInfo {
  hasNextPage
  endCursor
}

For backward pagination, use:

pageInfo {
  hasPreviousPage
  startCursor
}

If you provide all 4 properties in a pageInfo, githubkit will default to forward pagination.

Then, you can iterate over the paginated results by using the graphql paginate method:

for result in github.graphql.paginate(
    query, variables={"owner": "owner", "repo": "repo"}
):
    print(result)

Note that the result is a dict containing the list of nodes/edges for each page and the pageInfo object. You should iterate over the nodes or edges list to get the actual data. For example:

for result in g.graphql.paginate(query, {"owner": "owner", "repo": "repo"}):
    for issue in result["repository"]["issues"]["nodes"]:
        print(issue)

You can also provide a initial cursor value to start pagination from a specific point:

for result in github.graphql.paginate(
    query, variables={"owner": "owner", "repo": "repo", "cursor": "initial_cursor"}
):
    print(result)

[!NOTE] Nested pagination is not supported.

Auto Retry

By default, githubkit will retry the request when specific exception encountered. When rate limit exceeded, githubkit will retry once after GitHub suggested waiting time. When server error encountered (http status >= 500), githubkit will retry max three times.

You can disable this feature by set the auto_retry config to False:

github = GitHub(
    ...
    auto_retry=False
)

You can also customize the retry decision function by passing a callable:

from datetime import timedelta

from githubkit.retry import RetryOption
from githubkit.exception import GitHubException

def retry_decision_func(exc: GitHubException, retry_count: int) -> RetryOption:
    if retry_count < 1:
        return RetryOption(True, timedelta(seconds=60))
    return RetryOption(False)

github = GitHub(
    ...
    auto_retry=retry_decision_func
)

githubkit also provides some builtin retry decision function:

  1. Retry when rate limit exceeded:

    from githubkit.retry import RETRY_RATE_LIMIT, RetryRateLimit
    
    # default
    github = GitHub(
       ...
       auto_retry=RETRY_RATE_LIMIT
    )
    # or, custom max retry count
    github = GitHub(
       ...
       auto_retry=RetryRateLimit(max_retry=1)
    )
    
  2. Retry when server error encountered:

    from githubkit.retry import RETRY_SERVER_ERROR, RetryServerError
    
    # default
    github = GitHub(
       ...
       auto_retry=RETRY_SERVER_ERROR
    )
    # or, custom max retry count
    github = GitHub(
       ...
       auto_retry=RetryServerError(max_retry=1)
    )
    
  3. Chain retry decision functions:

    from githubkit.retry import RETRY_RATE_LIMIT, RETRY_SERVER_ERROR, RetryChainDecision
    
    github = GitHub(
       ...
       auto_retry=RetryChainDecision(RETRY_RATE_LIMIT, RETRY_SERVER_ERROR)
    )
    

Webhook Verification

githubkit.webhooks module contains some shortcut functions to help you verify and parse webhook payload.

Simple webhook payload verification:

from githubkit.webhooks import verify

valid: bool = verify(secret, request.body, request.headers["X-Hub-Signature-256"])

Sign the webhook payload manually:

from githubkit.webhooks import sign

signature: str = sign(secret, payload, method="sha256")

Webhook Parsing

githubkit.webhooks module contains some shortcut functions to help you verify and parse webhook payload.

Parse the payload with event name:

from githubkit.webhooks import parse

event = parse(request.headers["X-GitHub-Event"], request.body)

(NOT RECOMMENDED) Parse the payload without event name (may cost longer time and more memory):

from githubkit.webhooks import parse_without_name

event = parse_without_name(request.body)

[!WARNING] The parse_without_name function will try to parse the payload with all supported event names.
The behavior of this function is not the same between pydantic v1 and v2.
When using pydantic v1, the function will return the first valid event model (known as left-to-right mode).
When using pydantic v2, the function will return the highest scored valid event model (known as smart mode).
See: Union Modes.

Parse dict like payload:

from githubkit.webhooks import parse_obj, parse_obj_without_name

event = parse_obj(request.headers["X-GitHub-Event"], request.json())
event = parse_obj_without_name(request.json())  # NOT RECOMMENDED

The parse and parse_obj function supports type overload, if you provide static value for the event_name parameter, the return type will be inferred automatically.

Webhook also supports versioning, you can switch between versions as follows:

from githubkit import GitHub

event = GitHub.webhooks("2022-11-28").parse(request.headers["X-GitHub-Event"], request.body)

Switch between AuthStrategy (Installation, OAuth Web/Device Flow)

You can change the auth strategy and get a new client simplely using with_auth.

Change from AppAuthStrategy to AppInstallationAuthStrategy:

from githubkit import GitHub, AppAuthStrategy

github = GitHub(AppAuthStrategy("<app_id>", "<private_key>"))
installation_github = github.with_auth(
    github.auth.as_installation(installation_id)
)

Change from OAuthAppAuthStrategy to OAuthWebAuthStrategy (OAuth Web Flow):

from githubkit import GitHub, OAuthAppAuthStrategy

github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
user_github = github.with_auth(github.auth.as_web_user("<code>"))

# now you can act as the user
resp = user_github.rest.users.get_authenticated()
user = resp.parsed_data

# you can get the user token after you maked a request as user
user_token = user_github.auth.token
user_token_expire_time = user_github.auth.expire_time
refresh_token = user_github.auth.refresh_token
refresh_token_expire_time = user_github.auth.refresh_token_expire_time

you can also get the user token directly without making a request (Change from OAuthWebAuthStrategy to OAuthTokenAuthStrategy):

auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").exchange_token(github)
# or asynchronously
auth: OAuthTokenAuthStrategy = await github.auth.as_web_user("<code>").async_exchange_token(github)
user_token = auth.token
user_token_expire_time = auth.expire_time
refresh_token = auth.refresh_token
refresh_token_expire_time = auth.refresh_token_expire_time

user_github = github.with_auth(auth)

Change from OAuthDeviceAuthStrategy to OAuthTokenAuthStrategy:

from githubkit import GitHub, OAuthDeviceAuthStrategy

def callback(data: dict):
    print(data["user_code"])

user_github = GitHub(OAuthDeviceAuthStrategy("<client_id>", callback))

# now you can act as the user
resp = user_github.rest.users.get_authenticated()
user = resp.parsed_data

# you can get the user token after you maked a request as user
user_token = user_github.auth.token
user_token_expire_time = user_github.auth.expire_time
refresh_token = user_github.auth.refresh_token
refresh_token_expire_time = user_github.auth.refresh_token_expire_time

# you can also exchange the token directly without making a request
auth: OAuthTokenAuthStrategy = github.auth.exchange_token(github)
# or asynchronously
auth: OAuthTokenAuthStrategy = await github.auth.async_exchange_token(github)
user_token = auth.token
user_token_expire_time = auth.expire_time
refresh_token = auth.refresh_token
refresh_token_expire_time = auth.refresh_token_expire_time

user_github = github.with_auth(auth)

Development

Open in Codespaces (Dev Container):

Open in GitHub Codespaces

Generate latest models and apis:

[!WARNING] This may use about 400M memory and take a long time.

./scripts/run-codegen.sh

Run tests in dev env:

./scripts/run-tests.sh

Run tests in test envs, for example:

cd ./envs/pydantic-v2/
poetry run bash ../../scripts/run-tests.sh

Contributors

Thanks to the following people who have contributed to this project:

contributors

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

githubkit-0.11.11.tar.gz (1.9 MB view details)

Uploaded Source

Built Distribution

githubkit-0.11.11-py3-none-any.whl (5.2 MB view details)

Uploaded Python 3

File details

Details for the file githubkit-0.11.11.tar.gz.

File metadata

  • Download URL: githubkit-0.11.11.tar.gz
  • Upload date:
  • Size: 1.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.6

File hashes

Hashes for githubkit-0.11.11.tar.gz
Algorithm Hash digest
SHA256 575a601a80d2d2a4d1bb75f63a27f0e1c316d8f0d3e8bea8613af2681a4bf6fb
MD5 62cc26aba10ecff74c882aa0398ea4d8
BLAKE2b-256 da7a0a624dd6db45ea3b380c9fc31c82aa6d02399feacb0eb2739736f3612792

See more details on using hashes here.

File details

Details for the file githubkit-0.11.11-py3-none-any.whl.

File metadata

  • Download URL: githubkit-0.11.11-py3-none-any.whl
  • Upload date:
  • Size: 5.2 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.6

File hashes

Hashes for githubkit-0.11.11-py3-none-any.whl
Algorithm Hash digest
SHA256 1296548278c8e2a9aeff21f2415d1cbd7cb1aca337730789947c486bcaf580a8
MD5 9b178018abb81521d225b57cd3738c03
BLAKE2b-256 622569374f69859bcbb7e5ff201259e350d7a5d1261bf4f44caae0c01cd66fc1

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page