From f5091499d8cd38ec52c9b4aa635f1b8fb964194c Mon Sep 17 00:00:00 2001 From: Erik Gaasedelen Date: Thu, 15 Jan 2026 12:36:16 -0800 Subject: [PATCH 1/2] Add GraphQL support --- 05_graphql.ipynb | 203 +++++++++++++++++++++++++++++++++++++++++++++++ ghapi/_modidx.py | 13 +++ ghapi/graphql.py | 74 +++++++++++++++++ 3 files changed, 290 insertions(+) create mode 100644 05_graphql.ipynb create mode 100644 ghapi/graphql.py diff --git a/05_graphql.ipynb b/05_graphql.ipynb new file mode 100644 index 0000000..4365687 --- /dev/null +++ b/05_graphql.ipynb @@ -0,0 +1,203 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0ff34c42", + "metadata": {}, + "source": [ + "# graphql\n", + "\n", + "> GitHub GraphQL API support for ghapi" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe7862d1", + "metadata": {}, + "outputs": [], + "source": [ + "#| default_exp graphql" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "373423dc", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "import os\n", + "from gql import gql, Client\n", + "from gql.transport.requests import RequestsHTTPTransport" + ] + }, + { + "cell_type": "markdown", + "id": "a8ea827d", + "metadata": {}, + "source": [ + "## GhGql Client\n", + "\n", + "The `GhGql` class provides a lazy-loaded GraphQL client. The schema is fetched on first use, not at import time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "553fc866", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "#| export\n", + "class GhGql:\n", + " \"GitHub GraphQL client with lazy-loaded schema\"\n", + " def __init__(self, token=None):\n", + " self.token = token or os.getenv('GITHUB_TOKEN')\n", + " self._client = None\n", + " \n", + " @property\n", + " def client(self):\n", + " \"Lazy-load the GraphQL client and schema\"\n", + " if self._client is None:\n", + " if not self.token: raise ValueError(\"GITHUB_TOKEN not set\")\n", + " transport = RequestsHTTPTransport(\n", + " url='https://api.github.com/graphql',\n", + " headers={'Authorization': f'bearer {self.token}'}\n", + " )\n", + " self._client = Client(transport=transport, fetch_schema_from_transport=True)\n", + " return self._client\n", + " \n", + " @property\n", + " def schema(self): return self.client.schema\n", + " \n", + " def __call__(self, query, variables=None):\n", + " \"Execute a GraphQL query\"\n", + " return self.client.execute(gql(query), variable_values=variables)\n", + " \n", + " def list_queries(self):\n", + " \"List all available top-level GraphQL query types\"\n", + " return list(self.schema.query_type.fields.keys())\n", + " \n", + " def query_args(self, name):\n", + " \"Show arguments for a query type\"\n", + " return self.schema.query_type.fields[name].args\n", + " \n", + " def type_fields(self, name):\n", + " \"List fields on a GraphQL type (use PascalCase, e.g. 'Repository')\"\n", + " return list(self.schema.type_map[name].fields.keys())" + ] + }, + { + "cell_type": "markdown", + "id": "e7468f81", + "metadata": {}, + "source": [ + "## Module-level convenience functions\n", + "\n", + "These use a default client instance, similar to how powertools works." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1464c99", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "_default_client = None\n", + "\n", + "def _get_client():\n", + " \"Get or create the default client\"\n", + " global _default_client\n", + " if _default_client is None: _default_client = GhGql()\n", + " return _default_client\n", + "\n", + "def list_queries():\n", + " \"List all available top-level GraphQL query types\"\n", + " return _get_client().list_queries()\n", + "\n", + "def query_args(name):\n", + " \"Show arguments for a query type\"\n", + " return _get_client().query_args(name)\n", + "\n", + "def type_fields(name):\n", + " \"List fields on a GraphQL type (use PascalCase, e.g. 'Repository')\"\n", + " return _get_client().type_fields(name)\n", + "\n", + "def gh_query(query, variables=None):\n", + " \"Execute a GraphQL query\"\n", + " return _get_client()(query, variables)" + ] + }, + { + "cell_type": "markdown", + "id": "45ca0d7f", + "metadata": {}, + "source": [ + "## Examples" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17b59d28", + "metadata": {}, + "outputs": [], + "source": [ + "#| eval: false\n", + "# List available query types\n", + "list_queries()[:10]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b59fec62", + "metadata": {}, + "outputs": [], + "source": [ + "#| eval: false\n", + "# See what args a query takes\n", + "query_args('repository')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f073780a", + "metadata": {}, + "outputs": [], + "source": [ + "#| eval: false\n", + "# See fields on a type\n", + "type_fields('Repository')[:10]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "706870a7", + "metadata": {}, + "outputs": [], + "source": [ + "#| eval: false\n", + "# Execute a query\n", + "gh_query('''\n", + "query($owner: String!, $name: String!) {\n", + " repository(owner: $owner, name: $name) {\n", + " description\n", + " stargazerCount\n", + " }\n", + "}\n", + "''', {'owner': 'AnswerDotAI', 'name': 'ghapi'})" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ghapi/_modidx.py b/ghapi/_modidx.py index a4e04c0..25415b9 100644 --- a/ghapi/_modidx.py +++ b/ghapi/_modidx.py @@ -107,6 +107,19 @@ 'ghapi.event._want_evt': ('event.html#_want_evt', 'ghapi/event.py'), 'ghapi.event.load_sample_events': ('event.html#load_sample_events', 'ghapi/event.py'), 'ghapi.event.save_sample_events': ('event.html#save_sample_events', 'ghapi/event.py')}, + 'ghapi.graphql': { 'ghapi.graphql.GhGql': ('graphql.html#ghgql', 'ghapi/graphql.py'), + 'ghapi.graphql.GhGql.__call__': ('graphql.html#ghgql.__call__', 'ghapi/graphql.py'), + 'ghapi.graphql.GhGql.__init__': ('graphql.html#ghgql.__init__', 'ghapi/graphql.py'), + 'ghapi.graphql.GhGql.client': ('graphql.html#ghgql.client', 'ghapi/graphql.py'), + 'ghapi.graphql.GhGql.list_queries': ('graphql.html#ghgql.list_queries', 'ghapi/graphql.py'), + 'ghapi.graphql.GhGql.query_args': ('graphql.html#ghgql.query_args', 'ghapi/graphql.py'), + 'ghapi.graphql.GhGql.schema': ('graphql.html#ghgql.schema', 'ghapi/graphql.py'), + 'ghapi.graphql.GhGql.type_fields': ('graphql.html#ghgql.type_fields', 'ghapi/graphql.py'), + 'ghapi.graphql._get_client': ('graphql.html#_get_client', 'ghapi/graphql.py'), + 'ghapi.graphql.gh_query': ('graphql.html#gh_query', 'ghapi/graphql.py'), + 'ghapi.graphql.list_queries': ('graphql.html#list_queries', 'ghapi/graphql.py'), + 'ghapi.graphql.query_args': ('graphql.html#query_args', 'ghapi/graphql.py'), + 'ghapi.graphql.type_fields': ('graphql.html#type_fields', 'ghapi/graphql.py')}, 'ghapi.metadata': {}, 'ghapi.page': { 'ghapi.page.GhApi.last_page': ('page.html#ghapi.last_page', 'ghapi/page.py'), 'ghapi.page._Scanner': ('page.html#_scanner', 'ghapi/page.py'), diff --git a/ghapi/graphql.py b/ghapi/graphql.py new file mode 100644 index 0000000..9593435 --- /dev/null +++ b/ghapi/graphql.py @@ -0,0 +1,74 @@ +"""GitHub GraphQL API support for ghapi""" + +# AUTOGENERATED! DO NOT EDIT! File to edit: ../05_graphql.ipynb. + +# %% auto 0 +__all__ = ['GhGql', 'list_queries', 'query_args', 'type_fields', 'gh_query'] + +# %% ../05_graphql.ipynb 2 +import os +from gql import gql, Client +from gql.transport.requests import RequestsHTTPTransport + +# %% ../05_graphql.ipynb 4 +class GhGql: + "GitHub GraphQL client with lazy-loaded schema" + def __init__(self, token=None): + self.token = token or os.getenv('GITHUB_TOKEN') + self._client = None + + @property + def client(self): + "Lazy-load the GraphQL client and schema" + if self._client is None: + if not self.token: raise ValueError("GITHUB_TOKEN not set") + transport = RequestsHTTPTransport( + url='https://api.github.com/graphql', + headers={'Authorization': f'bearer {self.token}'} + ) + self._client = Client(transport=transport, fetch_schema_from_transport=True) + return self._client + + @property + def schema(self): return self.client.schema + + def __call__(self, query, variables=None): + "Execute a GraphQL query" + return self.client.execute(gql(query), variable_values=variables) + + def list_queries(self): + "List all available top-level GraphQL query types" + return list(self.schema.query_type.fields.keys()) + + def query_args(self, name): + "Show arguments for a query type" + return self.schema.query_type.fields[name].args + + def type_fields(self, name): + "List fields on a GraphQL type (use PascalCase, e.g. 'Repository')" + return list(self.schema.type_map[name].fields.keys()) + +# %% ../05_graphql.ipynb 6 +_default_client = None + +def _get_client(): + "Get or create the default client" + global _default_client + if _default_client is None: _default_client = GhGql() + return _default_client + +def list_queries(): + "List all available top-level GraphQL query types" + return _get_client().list_queries() + +def query_args(name): + "Show arguments for a query type" + return _get_client().query_args(name) + +def type_fields(name): + "List fields on a GraphQL type (use PascalCase, e.g. 'Repository')" + return _get_client().type_fields(name) + +def gh_query(query, variables=None): + "Execute a GraphQL query" + return _get_client()(query, variables) From b38cb04dd359c7fa65fa8637df81265f4b2565c9 Mon Sep 17 00:00:00 2001 From: Erik Gaasedelen Date: Thu, 15 Jan 2026 14:13:37 -0800 Subject: [PATCH 2/2] Trigger schema fetch with dummy query --- 05_graphql.ipynb | 8 +++++--- ghapi/graphql.py | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/05_graphql.ipynb b/05_graphql.ipynb index 4365687..6b08dc7 100644 --- a/05_graphql.ipynb +++ b/05_graphql.ipynb @@ -68,6 +68,7 @@ " headers={'Authorization': f'bearer {self.token}'}\n", " )\n", " self._client = Client(transport=transport, fetch_schema_from_transport=True)\n", + " self._client.execute(gql('{ __typename }')) # Trigger schema fetch\n", " return self._client\n", " \n", " @property\n", @@ -107,6 +108,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "#| export\n", "_default_client = None\n", "\n", @@ -120,15 +122,15 @@ " \"List all available top-level GraphQL query types\"\n", " return _get_client().list_queries()\n", "\n", - "def query_args(name):\n", + "def query_args(name:str):\n", " \"Show arguments for a query type\"\n", " return _get_client().query_args(name)\n", "\n", - "def type_fields(name):\n", + "def type_fields(name:str):\n", " \"List fields on a GraphQL type (use PascalCase, e.g. 'Repository')\"\n", " return _get_client().type_fields(name)\n", "\n", - "def gh_query(query, variables=None):\n", + "def gh_query(query:str, variables:dict[str,any]=None):\n", " \"Execute a GraphQL query\"\n", " return _get_client()(query, variables)" ] diff --git a/ghapi/graphql.py b/ghapi/graphql.py index 9593435..e7cba37 100644 --- a/ghapi/graphql.py +++ b/ghapi/graphql.py @@ -27,6 +27,7 @@ def client(self): headers={'Authorization': f'bearer {self.token}'} ) self._client = Client(transport=transport, fetch_schema_from_transport=True) + self._client.execute(gql('{ __typename }')) # Trigger schema fetch return self._client @property @@ -61,14 +62,14 @@ def list_queries(): "List all available top-level GraphQL query types" return _get_client().list_queries() -def query_args(name): +def query_args(name:str): "Show arguments for a query type" return _get_client().query_args(name) -def type_fields(name): +def type_fields(name:str): "List fields on a GraphQL type (use PascalCase, e.g. 'Repository')" return _get_client().type_fields(name) -def gh_query(query, variables=None): +def gh_query(query:str, variables:dict[str,any]=None): "Execute a GraphQL query" return _get_client()(query, variables)