How-to guides

Running a one-off task against GitHub API

Sometimes you need to run a series of queries against GitHub API. To do this, initialize a token (it’s taken from the GITHUB_TOKEN env var in the example below), constuct a GitHub API wrapper and you are good to go.

RawGitHubAPI is a wrapper around interface provided by GitHubAPI, you can find the usage interface on its documentation page. Don’t forget to specify a user_agent string — it’s mandatory!

API calls return native Python dict or iterable objects.

import asyncio
import os

from octomachinery.github.api.tokens import GitHubOAuthToken
from octomachinery.github.api.raw_client import RawGitHubAPI


async def main():
    access_token = GitHubOAuthToken(os.environ["GITHUB_TOKEN"])
    github_api = RawGitHubAPI(access_token, user_agent='webknjaz')
    await github_api.post(
        '/repos/mariatta/strange-relationship/issues',
        data={
            'title': 'We got a problem',
            'body': 'Use more emoji!',
        },
    )


asyncio.run(main())

Authenticating as a bot (GitHub App)

To act as a bot, you should use a special kind of integration with GitHub — Apps. They are reusable entities in the GitHub Platform available to be installed into multiple accounts and organizations.

Classic GitHub App requires a web-sever part to be deployed somewhere on the Internet in order to receive events GitHub Platform would send there.

Yet, sometimes, you just want to act as a bot without any of that deployment hustle. You may want to have a [bot] label next to comments you’ll post via API. You may want to manage rate limits better. This will allow you to run one-off tasks like batch changes/migrations.

You’ll still need to register a GitHub App and install it into the target user account or organization. You’ll also have to specify APIs you’d like to access using this App.

Then, you’ll need to get your App’s ID and a private key.

Now, first, specify the App ID, the key path and the target account. After that, create a GitHubAppIntegrationConfig instance also specifying app name, version and some URL (these will be used to generate a User-Agent HTTP header for API queries). Then, create a GitHubApp instance from that config. Retrieve a list of places where the App is installed, filter out the target Installation and get an API client for it. Finally, use RawGitHubAPI as usual.

import asyncio
import pathlib

from octomachinery.github.api.app_client import GitHubApp
from octomachinery.github.config.app import GitHubAppIntegrationConfig


target_github_account_or_org = 'webknjaz'  # where the app is installed to

github_app_id = 12345
github_app_private_key_path = pathlib.Path(
    '~/Downloads/star-wars.2011-05-04.private-key.pem',
).expanduser().resolve()

github_app_config = GitHubAppIntegrationConfig(
    app_id=github_app_id,
    private_key=github_app_private_key_path.read_text(),

    app_name='MyGitHubClient',
    app_version='1.0',
    app_url='https://awesome-app.dev',
)
github_app = GitHubApp(github_app_config)


async def get_github_client(account):
    github_app_installations = await github_app.get_installations()
    target_github_app_installation = next(  # find the one
        (
            i for n, i in github_app_installations.items()
            if i._metadata.account['login'] == account
        ),
        None,
    )
    return target_github_app_installation.get_github_api_client()


async def main():
    github_api = await get_github_client(target_github_account_or_org)
    user = await github_api.getitem(
        f'/users/{target_github_account_or_org}',
    )
    print(f'User found: {user["login"]}')
    print(f'Rate limit stats: {github_api.rate_limit!s}')


asyncio.run(main())

Making API queries against preview endpoints

Endpoints with stable interfaces in GitHub API are easy to hit. But some are marked as preview API. For those, GitHub requires special Accept headers to be passed along with a normal HTTP request. The exact strings are documented at https://developers.github.com under specific endpoint sections in their description.

Given that you’ve already got an instance of RawGitHubAPI initialized, what’s left is to pass preview_api_version argument with the appropriate preview API code name when making query to the API endpoint requiring that.

github_api: RawGitHubAPI

repo_slug = 'sanitizers/octomachinery'
issue_number = 15

await github_api.post(
    f'/repos/{repo_slug}/issues/{issue_number}/reactions',
    preview_api_version='squirrel-girl',
    data={'content': 'heart'},
)