from fastcore.test import *
from pyskills import xdir, docgraphql
GitHub’s GraphQL API lets you request specific fields from several resources in one query. For example, you can fetch the head commits of many repositories without a separate REST request for each repository.
GhGql uses fastspec’s GraphQL client with GitHub’s schema. Set GITHUB_TOKEN to get started. Explore fields with xdir, attribute completion and rich displays. Build query fragments by chaining attributes and passing keyword arguments. The client checks field names against the bundled schema.
You can also use raw GraphQL for complete queries or selections within a field. Attributes expose query fields. Mutations require raw GraphQL text.
The distilled schema
build_gql_spec in build_lib introspects GitHub’s schema at build time. It uses fastspec.gql to save compact tables in ghapi.gql_spec. The client loads these tables without a network request, as it does for the bundled REST spec.
The shipped tables for GitHub’s real schema (1.35MB on disk, ~180KB in the wheel):
from ghapi.gql_spec import gqlspeclen(gqlspec['types']), gqlspec['query'], gqlspec['types']['Ref']['fields']['target']['type'](1813, 'Query', 'GitObject')
GhGql configures GqlClient with GitHub’s endpoint and the bundled schema. Authentication uses GITHUB_TOKEN or an explicit token=. It inherits fragments, batch, gql.t and raw queries. GqlError retains the server’s errors and any partial data.
GhGql
def GhGql(
token:NoneType=None
):GitHub GraphQL client using the shipped distilled schema
Discovery
Discovery uses the same objects as query construction. xdir lists what the schema allows next. doc on an unfinished fragment shows its signature, argument descriptions, and the fields available on its result type. Constructing and documenting a fragment sends no request:
gql = GhGql()
xdir(gql, 'repo')['repository', 'repositoryOwner']
doc(gql.repository)```text
repository(owner: String!, name: String!, followRenames: Boolean = true) -> Repository
Lookup a given repository by the owner and repository name.
args:
owner: The login field of a user or organization
name: The name of the repository
followRenames: Follow repository renames. If disabled, a repository referenced by its old name will return an error.
fields of Repository: allowUpdateBranch archivedAt assignableUsers autoMergeAllowed branchProtectionRules codeOfConduct codeowners collaborators commitComments contactLinks contributingGuidelines createdAt databaseId defaultBranchRef deleteBranchOnMerge dependencyGraphManifests deployKeys deployments description descriptionHTML ...
```
A GraphQL query path: attribute access extends it, calling binds args, awaiting executes.
Call with keyword arguments to bind field arguments, or with a selection string for branching selections. A raw selection ends chaining. Use `xdir(fragment)` for available fields and `client.t.TypeName` for schema types. Await only a leaf or a fragment with a selection.
A complete fragment displays the GraphQL query it will send. Await it to execute the query. The result is the value at the end of the path:
f = gql.repository(owner='AnswerDotAI', name='fastws').ref(qualifiedName='refs/heads/main').target.oid
f{ repository(owner: "AnswerDotAI", name: "fastws") { ref(qualifiedName: "refs/heads/main") { target { oid } } } }
A GraphQL query path: attribute access extends it, calling binds args, awaiting executes.
Call with keyword arguments to bind field arguments, or with a selection string for branching selections. A raw selection ends chaining. Use xdir(fragment) for available fields and client.t.TypeName for schema types. Await only a leaf or a fragment with a selection.
sha = await f
test_eq(len(sha), 40)
sha'd64c351ed5c9d7da6a596a7cf5d79647ee06862e'
Batching
batch accepts query fragments as separate arguments or one iterable. Results keep the input order. repo builds a repository fragment from an 'owner/name' string.
GhGql sends batches of up to 25 fragments concurrently. GitHub resolves aliases within each query serially. In a measurement with 103 repositories, one query took 5.5 seconds and chunked requests took 1.7 seconds.
Fetch the repositories’ head commits in one batch call:
repos = [('AnswerDotAI/fastws', 'main'), ('AnswerDotAI/ghapi', 'main'), ('fastai/fastcore', 'main'),
('AnswerDotAI/aidialog', 'main'), ('AnswerDotAI/llmdojo', 'main')]
heads = await gql.batch(gql.repo(s).ref(qualifiedName=f'refs/heads/{b}').target.oid for s, b in repos)
test_eq(len(heads), len(repos))
for h in heads: test_eq(len(h), 40)
heads['d64c351ed5c9d7da6a596a7cf5d79647ee06862e',
'ad07893daac86de6693bc9bb57ae7216c1b347d0',
'25c4f3228ccac3c5a63da71b5eaa4be3c428f602',
'53913aba7c6ef152294953645a5dbb9dee142276',
'1ea3915e8cc7f348e4ef3bfc855075ab5368cfc2']
A missing repository produces an error for its query alias. batch returns None for that result and keeps the other results. Errors affecting the whole query, such as invalid syntax or authentication, raise GqlError:
res = await gql.batch(*[gql.repository(owner='AnswerDotAI', name=n).ref(qualifiedName='refs/heads/main').target.oid
for n in ('fastws', 'no-such-repo-xyz', 'ghapi')])
test_eq(res[1], None)
test_eq(len(res[0]), 40)
res['d64c351ed5c9d7da6a596a7cf5d79647ee06862e',
None,
'ad07893daac86de6693bc9bb57ae7216c1b347d0']
Raw selections
Use raw GraphQL selections to request multiple fields or select a member of a union or interface. Here object returns the GitObject interface. ... on Blob selects its text field when the object is a blob.
This fetches pyproject.toml from multiple repositories in one request. dep_graph in ghapi.core fetches these files with sequential REST calls:
frags = [gql.repository(owner='AnswerDotAI', name=n).object(expression='HEAD:pyproject.toml')('... on Blob { text }')
for n in ('fastws', 'aidialog')]
blobs = await gql.batch(*frags)
assert all('[project]' in b.text for b in blobs)
print(blobs[0].text[:120])[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "fastws-
ProjectsV2 boards are available through GraphQL:
await gql.organization(login='github').projectsV2(first=3, orderBy=dict(field='TITLE', direction='ASC'))('nodes { title public }'){ 'nodes': [{'title': 'BUGS', 'public': True}, {'title': 'Campus Experts + GitHub Docs program', 'public': True}, {'title': 'Coding Standards Public Development Board', 'public': True}]}Connection fields accept at most 100 items per page. paged follows Relay cursors to fetch the rest. Here we collect every repository name in the organization:
names = [o.name async for o in gql.paged(gql.organization(login='AnswerDotAI').repositories, 'name')]
assert len(names) > 100
len(names)547
The type index
Use gql.t to look up schema types by name. It shows enum values, input-object fields and union members:
doc(gql.t.OrderDirection)```text
OrderDirection (ENUM)
Possible directions in which to order a list of items when provided an `orderBy` argument.
ASC: Specifies an ascending order for a given `orderBy` argument.
DESC: Specifies a descending order for a given `orderBy` argument.
```
Raw queries and errors
Call the client with raw GraphQL and variable values. Use this for mutations too:
res = await gql('query($owner: String!) { organization(login: $owner) { name createdAt } }', owner='AnswerDotAI')
test_eq(res.organization.name, 'Answer.AI')
res{'organization': {'createdAt': '2024-01-13T09:43:28Z', 'name': 'Answer.AI'}}The client rejects an unfinished fragment before sending a request. It also rejects unknown fields while building a query. A direct query for a missing repository raises GqlError with the server’s message:
r = gql.repository(owner='AnswerDotAI', name='fastws')
with expect_fail(TypeError, contains='needs a selection'): await r
with expect_fail(AttributeError, contains='has no field'): r.no_such_field
with expect_fail(GqlError, contains='Could not resolve'):
await gql.repository(owner='AnswerDotAI', name='no-such-repo-xyz').ref(qualifiedName='refs/heads/main').target.oid