GhApi details

Detailed information on the GhApi API

You can set an environment variable named GH_HOST to override the default of https://api.github.com incase you are running GitHub Enterprise(GHE). However, this library has not been tested on GHE, so proceed at your own risk.


source

GhApi


def GhApi(
    owner:NoneType=None, repo:NoneType=None, token:NoneType=None, jwt_token:NoneType=None, debug:NoneType=None,
    limit_cb:NoneType=None, gh_host:NoneType=None, authenticate:bool=True, kwargs:VAR_KEYWORD
):

Initialize self. See help(type(self)) for accurate signature.

Access by path


source

GhApi.__call__


def __call__(
    path:str, verb:str=None, headers:dict=None, route:dict=None, query:dict=None, data:NoneType=None,
    timeout:NoneType=None, decode:bool=True
):

Call a fully specified path using HTTP verb, passing arguments to fastcore.core.urlsend

api = GhApi()

You can call a GhApi object as a function, passing in the path to the endpoint, the HTTP verb, and any route, query parameter, or post data parameters as required.

api('/repos/{owner}/{repo}/git/ref/{ref}', 'GET', route=dict(
    owner='fastai', repo='ghapi-test', ref='heads/master'))
{ 'node_id': 'MDM6UmVmMzE1NzEyNTg4OnJlZnMvaGVhZHMvbWFzdGVy',
  'object': { 'sha': 'b72d6c87a9237ca3c26298a64a6acf06217ace4a',
              'type': 'commit',
              'url': 'https://api.github.com/repos/fastai/ghapi-test/git/commits/b72d6c87a9237ca3c26298a64a6acf06217ace4a'},
  'ref': 'refs/heads/master',
  'url': 'https://api.github.com/repos/fastai/ghapi-test/git/refs/heads/master'}

source

GhApi.__getitem__


def __getitem__(
    k
):

Lookup and call an endpoint by path and verb (which defaults to ‘GET’)

You can access endpoints by indexing into the object. When using the API this way, you do not need to specify what type of parameter (route, query, or post data) is being used. This is, therefore, the same call as above:

api['/repos/{owner}/{repo}/git/ref/{ref}'](owner='fastai', repo='ghapi-test', ref='heads/master')
{ 'node_id': 'MDM6UmVmMzE1NzEyNTg4OnJlZnMvaGVhZHMvbWFzdGVy',
  'object': { 'sha': 'b72d6c87a9237ca3c26298a64a6acf06217ace4a',
              'type': 'commit',
              'url': 'https://api.github.com/repos/fastai/ghapi-test/git/commits/b72d6c87a9237ca3c26298a64a6acf06217ace4a'},
  'ref': 'refs/heads/master',
  'url': 'https://api.github.com/repos/fastai/ghapi-test/git/refs/heads/master'}

Media types

For some endpoints GitHub lets you specify a media type the for response data, using the Accept header. If you choose a media type that is not JSON formatted (for instance application/vnd.github.v3.sha) then the call to the GhApi object will return a string instead of an object.

api('/repos/{owner}/{repo}/commits/{ref}', 'GET', route=dict(
    owner='fastai', repo='ghapi-test', ref='refs/heads/master'),
    headers={'Accept': 'application/vnd.github.VERSION.sha'})
'b72d6c87a9237ca3c26298a64a6acf06217ace4a'

Rate limits

GitHub has various rate limits for their API. After each call, the response includes information about how many requests are remaining in the hourly quota. If you’d like to add alerts, or indications showing current quota usage, you can register a callback with GhApi by passing a callable to the limit_cb parameter. This callback will be called whenever the amount of quota used changes. It will be called with two arguments: the new quota remaining, and the total hourly quota.

def _f(rem,quota): print(f"Quota remaining: {rem} of {quota}")

api = GhApi(limit_cb=_f)
api['/repos/{owner}/{repo}/git/ref/{ref}'](owner='fastai', repo='ghapi-test', ref='heads/master').ref
Quota remaining: 4799 of 5000
'refs/heads/master'

You can always get the remaining quota from the limit_rem attribute:

api.limit_rem
'4799'

Operations

Instead of passing a path to GhApi, you will more often use the operation methods provided in the API’s operation groups, which include documentation, signatures, and auto-complete.

If you provide owner and/or repo to the constructor, they will be automatically inserted into any calls which use them (except when calling GhApi as a function). You can also pass any other arbitrary keyword arguments you like to have them used as defaults for any relevant calls.

You must include a GitHub API token if you need to access any authenticated endpoints. If don’t pass the token param, then your GITHUB_TOKEN environment variable will be used, if available.

api = GhApi(owner='AnswerDotAI', repo='ghapi-test', token=token)

Operation groups

The following groups of endpoints are provided, which you can list at any time along with a link to documentation for all endpoints in that group, by displaying the GhApi object:

api.codes_of_conduct

Calling endpoints

The GitHub API’s endpoint names generally start with a verb like “get”, “list”, “delete”, “create”, etc, followed _, then by a noun such as “ref”, “webhook”, “issue”, etc.

Each endpoint has a different signature, which you can see by using Shift-Tab in Jupyter, or by just printing the endpoint object (which also shows a link to the GitHub docs):

print(api.repos.create_webhook)
repos.create_webhook(name: str = None, config: dict = None, events: list = ['push'], active: bool = True)
https://docs.github.com/rest/repos/webhooks#create-a-repository-webhook

Displaying an endpoint object in Jupyter also provides a formatted summary and link to the official GitHub documentation:

api.repos.create_webhook

repos.create_webhook(name, config, events, active): Create a repository webhook

Endpoint objects are called using standard Python method syntax:

ref = api.git.get_ref('heads/master')
test_eq(ref.object.type, 'commit')

Information about the endpoint are available as attributes:

api.git.get_ref.path,api.git.get_ref.verb
('/repos/AnswerDotAI/ghapi-test/git/ref/{ref}', 'get')

You can get a list of all endpoints available in a group, along with a link to documentation for each, by viewing the group:

api.git

For “list” endpoints, the noun will be a plural form, e.g.:

hooks = api.repos.list_webhooks()
test_eq(len(hooks), 0)

You can pass dicts, lists, etc. directly, where they are required for GitHub API endpoints:

url = 'https://example.com'
cfg = dict(url=url, content_type='json', secret='XXX')
hook = api.repos.create_webhook(config=cfg, events=['ping'])
test_eq(hook.config.url, url)

Let’s confirm that our new webhook has been created:

hooks = api.repos.list_webhooks()
test_eq(len(hooks), 1)
test_eq(hooks[0].events, ['ping'])

Finally, we can delete our new webhook:

api.repos.delete_webhook(hooks[0].id)
{}

Convenience functions


source

date2gh


def date2gh(
    dt:datetime
)->str:

Convert dt (which is assumed to be in UTC time zone) to a format suitable for GitHub API operations

The GitHub API assumes that dates will be in a specific string format. date2gh converts Python standard datetime objects to that format. For instance, to find issues opened in the ‘fastcore’ repo in the last 4 weeks:

dt = date2gh(datetime.now(timezone.utc) - timedelta(weeks=4))
issues = GhApi('fastai').issues.list_for_repo(repo='fastcore', since=dt)
len(issues)
4

source

gh2date


def gh2date(
    dtstr:str
)->datetime:

Convert date string dtstr received from a GitHub API operation to a UTC datetime

# created = issues[0].created_at
# print(created, '->', gh2date(created))

You can set the debug attribute to any callable to intercept all requests, for instance to print Request.summary. print_summary is provided for this purpose. Using this, we can see the preview header that is added for preview functionality, e.g.

api.debug=print_summary
api.codes_of_conduct.get_all_codes_of_conduct()[0]
api.debug=None
{'data': None,
 'full_url': 'https://api.github.com/codes_of_conduct',
 'headers': {'Accept': 'application/vnd.github.v3+json'},
 'method': 'GET'}

Preview endpoints

GitHub’s preview API functionality requires a special header to be passed to enable it. This is added automatically for you.

Convenience methods

Some methods in the GitHub API are a bit clunky or unintuitive. In these situations we add convenience methods to GhApi to make things simpler. There are also some multi-step processes in the GitHub API that GhApi provide convenient wrappers for. The methods currently available are shown below; do not hesitate to create an issue or pull request if there are other processes that you’d like to see supported better.


source

GhApi.create_gist


def create_gist(
    description, content, filename:str='gist.txt', public:bool=False, img_paths:NoneType=None
):

Create a gist, optionally with images where each md img url will be placed with img upload urls.

gist = api.create_gist("some description", "some content")
print(gist.html_url)
https://gist.github.com/KeremTurgutlu/299c624d1175231e3d92e91be81afb41
gist.files['gist.txt'].content
'some content'
gist = api.create_gist("some description", "some image\n\n![image](puppy.jpg)", 'gist.md', img_paths=['puppy.jpg'])
print(gist.html_url)
https://gist.github.com/KeremTurgutlu/5e4197bbcaa63d426b990a841ed55d49
gist.files['gist.md'].content
'some image\n\n![image](https://gist.githubusercontent.com/KeremTurgutlu/5e4197bbcaa63d426b990a841ed55d49/raw/c7f420c839f58c6ac0c05f1116317645d31d7e80/puppy.jpg)'

Note that if you want to create a gist with multiple files, call the GitHub API directly, e.g.:

api.gists.create("some description", files={"f1.txt": {"content": "my content"}, ...})

Releases


source

GhApi.delete_release


def delete_release(
    release
):

Delete a release and its associated tag


source

GhApi.upload_file


def upload_file(
    rel, fn
):

Upload fn to endpoint for release rel


source

GhApi.create_release


def create_release(
    tag_name, branch:str='master', name:NoneType=None, body:str='', draft:bool=False, prerelease:bool=False,
    files:NoneType=None
):

Wrapper for GhApi.repos.create_release which also uploads files

Creating a release and attaching files to it is normally a multi-stage process, so create_release wraps this up for you. It takes the same arguments as repos.create_release, along with files, which can contain a single file name, or a list of file names to upload to your release:

rel = api.create_release('0.0.1', files=['README.md'])
test_eq(rel.name, 'v0.0.1')
sleep(0.2)
rels = api.repos.list_releases()
test_eq(len(rels), 1)

We can check that our file has been uploaded; GitHub refers to them as “assets”:

assets = api.repos.list_release_assets(rels[0].id)
test_eq(assets[0].name, 'README.md')

source

GhApi.delete_release


def delete_release(
    release
):

Delete a release and its associated tag

Branches and tags


source

GhApi.list_tags


def list_tags(
    prefix:str=''
):

List all tags, optionally filtered to those starting with prefix

With no prefix, all tags are listed.

test_eq(len(api.list_tags()), 1)

Using the full tag name will return just that tag.

test_eq(len(api.list_tags(rel.tag_name)), 1)

source

GhApi.list_branches


def list_branches(
    prefix:str=''
):

List all branches, optionally filtered to those starting with prefix

Branches can be listed in the exactly the same way as tags.

test_eq(len(api.list_branches('master')), 1)

We can delete our release and confirm that it is removed:

api.delete_release(rels[0])
test_eq(len(api.repos.list_releases()), 0)
# #| hide
# #not working
# #| export
# @patch
# def create_branch_empty(self:GhApi, branch):
#     c = self.git.create_commit(f'create {branch}', EMPTY_TREE_SHA)
#     return self.git.create_ref(f'refs/heads/{branch}', c.sha)

source

GhApi.create_branch_empty


def create_branch_empty(
    branch
):
ref = api.create_branch_empty("testme")
test_eq(len(api.list_branches('testme')), 1)

source

GhApi.delete_tag


def delete_tag(
    tag:str
):

Delete a tag


source

GhApi.delete_branch


def delete_branch(
    branch:str
):

Delete a branch

api.delete_branch('testme')
test_eq(len(api.list_branches('testme')), 0)

source

GhApi.get_branch


def get_branch(
    branch:NoneType=None
):

Content (git files)


source

GhApi.list_files


def list_files(
    branch:NoneType=None
):
files = api.list_files()
files['README.md']
{ 'mode': '100644',
  'path': 'README.md',
  'sha': 'eaea0f2698e76c75602058bf4e2e9fd7940ac4e3',
  'size': 72,
  'type': 'blob',
  'url': 'https://api.github.com/repos/AnswerDotAI/ghapi-test/git/blobs/eaea0f2698e76c75602058bf4e2e9fd7940ac4e3'}

source

GhApi.get_content


def get_content(
    path
):
readme = api.get_content('README.md').decode()
assert 'ghapi' in readme

source

GhApi.create_or_update_file


def create_or_update_file(
    path, message, committer, author, content:NoneType=None, sha:NoneType=None, branch:str=''
):

source

GhApi.create_file


def create_file(
    path, message, committer, author, content:NoneType=None, branch:NoneType=None
):
person = dict(name="Monalisa Octocat", email="[email protected]")
res = api.create_file(
    path='foo',
    message="Create foo",
    content="foobar",
    committer=person, author=person
)
test_eq('foobar', api.get_content('foo').decode())

source

GhApi.delete_file


def delete_file(
    path, message, committer, author, sha:NoneType=None, branch:NoneType=None
):
api.delete_file('foo', 'delete foo', committer=person, author=person)
assert 'foo' not in api.list_files()

source

GhApi.update_contents


def update_contents(
    path, message, committer, author, content, sha:NoneType=None, branch:NoneType=None
):
res = api.update_contents(
    path='README.md',
    message="Update README",
    committer=person, author=person,
    content=readme+"foobar"
)
res.content.size
78
readme = api.get_content('README.md').decode()
assert 'foobar' in readme
api.update_contents('README.md', "Revert README", committer=person, author=person, content=readme[:-6]);
api = GhApi(token=token)
owner, repo, branch = "AnswerDotAI", "fastcore", "main"

Repo files can be filtered using fnmatch Unix shell-style wildcards.

_find_matches('README.md', ['*.py', '*test_*', '*/test*/*', '*.md', 'README.md'])
['*.md', 'README.md']

The include/exclude logic follows the rsync/grep model: a file must match at least one include pattern (if specified), AND must not match any exclude pattern. Exclude always wins—there’s no ambiguity. This is simpler and more predictable than gitignore-style ordering rules. Additionally, LLMs are already familiar with this common pattern from tools like rg and rsync, making it natural to use when this function is provided as an AI tool.

With rsync/grep style, exclude always wins. To get “all .md except README.md”, you’d include README.md explicitly in your results separately.

assert not _include('README.md', ['README.md'], ['*.md'])  # exclude wins
assert not _include('CONTRIBUTING.md', ['README.md'], ['*.md'])

Include all .py files except for tests

assert not _include('examples/test_fastcore2.py', ['*.py'], ['*test_*', '*/test*/*'])
assert not _include('examples/tests/some_test.py', ['*.py'], ['*test_*', '*/tests/*'])
assert not _include('examples/test/some_test.py', ['*.py'], ['*test_*', '*/test/*'])
assert _include('cool/module.py', ['*.py'], ['setup.py'])
assert not _include('cool/_modidx', ['*.py'], ['*/_modidx'])
assert not _include('setup.py', ['*.py'], ['setup.py'])
test_repo_files = ['README.md', 'CONTRIBUTING.md', 'dir/MARKDOWN.md', 'tests/file.py', 
                   'module/file.py', 'module/app/file.py', 'nbs/00.ipynb', 'file2.py',
                   '.gitignore', 'module/.dotfile', '_hidden.py', 'module/_hidden.py']

Here is an example where we filter to include the README, all python files except for the ones under tests directory, include all notebooks, and exclude all files starting with an underscore.

inc,exc = ['README.md', '*.py', '*.ipynb'], ['tests/*.py', '_*', '*/_*']
[fn for fn in test_repo_files if _include(fn,inc,exc)]
['README.md',
 'module/file.py',
 'module/app/file.py',
 'nbs/00.ipynb',
 'file2.py']

Let’s exclude files starting with test_ and setup.py too.

exc += ['*test_*.py', '*/*test*.py', 'setup.py']; exc
['tests/*.py', '_*', '*/_*', '*test_*.py', '*/*test*.py', 'setup.py']

A function to get repo files with optional filtering


source

GhApi.get_repo_files


def get_repo_files(
    owner, repo, branch:str='main', inc:NoneType=None, exc:NoneType=None
):

Get all file items of a repo, optionally filtered.

The list of files that are kept based on the filtering logic:

repo_files = api.get_repo_files(owner, repo, inc=inc, exc=exc)
test_eq(len(repo_files), 41); repo_files.attrgot("path")
['README.md', 'fastcore/all.py', 'fastcore/ansi.py', 'fastcore/basics.py', 'fastcore/dispatch.py', 'fastcore/docments.py', 'fastcore/docscrape.py', 'fastcore/foundation.py', 'fastcore/imghdr.py', 'fastcore/imports.py', 'fastcore/meta.py', 'fastcore/nb_imports.py', 'fastcore/net.py', 'fastcore/parallel.py', 'fastcore/py2pyi.py', 'fastcore/script.py', 'fastcore/shutil.py', 'fastcore/style.py', 'fastcore/tools.py', 'fastcore/transform.py', 'fastcore/utils.py', 'fastcore/xdg.py', 'fastcore/xml.py', 'fastcore/xtras.py', 'nbs/000_tour.ipynb', 'nbs/00_test.ipynb', 'nbs/01_basics.ipynb', 'nbs/02_foundation.ipynb', 'nbs/03_xtras.ipynb', 'nbs/03a_parallel.ipynb', 'nbs/03b_net.ipynb', 'nbs/04_docments.ipynb', 'nbs/05_meta.ipynb', 'nbs/06_script.ipynb', 'nbs/07_xdg.ipynb', 'nbs/08_style.ipynb', 'nbs/09_xml.ipynb', 'nbs/10_py2pyi.ipynb', 'nbs/11_external.ipynb', 'nbs/12_tools.ipynb', 'nbs/index.ipynb']

source

GhApi.get_file_content


def get_file_content(
    path, owner, repo, branch:str='main'
):

source

GhApi.get_repo_contents


def get_repo_contents(
    owner, repo, branch:str='main', inc:NoneType=None, exc:NoneType=None
):
contents = api.get_repo_contents(owner, repo, inc=inc, exc=exc)
md = "\n\n".join(f"**[{o.path}]({o.html_url})**\n```{o.path.split('.')[-1]}\n{chr(10).join(o.content_decoded.split(chr(10))[:5])}\n```" for o in contents[:3])
display(Markdown(md))

README.md

# Welcome to fastcore


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

fastcore/all.py

from .imports import *
from .foundation import *
from .utils import *
from .parallel import *
from .net import *

fastcore/ansi.py

"Filters for processing ANSI colors."

# Copyright (c) IPython Development Team.
# Modifications by Jeremy Howard.

GitHub Pages


source

GhApi.enable_pages


def enable_pages(
    branch:NoneType=None, path:str='/'
):

Enable or update pages for a repo to point to a branch and path.

branch is set to the default branch if None. path must be /docs or /.

api = GhApi(owner='AnswerDotAI', repo='ghapi-test', token=token)
res = api.enable_pages(branch='new-branch', path='/')

test_eq(res.source.branch, 'new-branch')
test_eq(res.source.path, '/')

api.repos.delete_pages_site()
api.delete_branch('new-branch')
{}