!pip install pandas gql requests_toolbelt
import pandas as pd
from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport
GraphQL Examples#
In this notebook we explore searching for metadata from the GraphQL API. The GraphQL API provides a method to programmatically extract a JSON representation of the meta data from the API. It has the advantage over REST that it can prevent under and over fetching data.
First we load some python dependancies that we will use as part of this notebook and set the variable API_URL to the location of the GraphQL API.
# This is the location of the GraphQL API
API_URL = "https://mastapp.site/graphql"
Setup a gql client. This is used to query the graphql API
# Select your transport with a defined url endpoint
transport = RequestsHTTPTransport(url=API_URL)
# Create a GraphQL client using the defined transport
client = Client(transport=transport, fetch_schema_from_transport=True)
Querying Shots with GraphQL#
With GraphQL you can query exactly what you want, rather than having to recieve the whole table from the database This is useful in cases where the whole table has many columns, but you are interested in just a subset of them.
The GraphQL endpoint is located at /graphql. You can find the documentation and an interactive query explorer at the URL below:
print(f"GraphQL API Endpoint {API_URL}")
GraphQL API Endpoint https://mastapp.site/graphql
Unlike the REST API which uses HTTP GET requests to return data, with GraphQL we use HTTP POST to post our query to the API.
Here is a simple example of getting some shot data from the GraphQL API. We need to explicity state what information we want to return from the API. Here we are asking for:
the shot ID
the timestamp that the shot was taken
the preshot description
the divertor configuration
# Write our GraphQL query.
query = gql("""
query {
all_shots {
shots {
shot_id
timestamp
preshot_description
divertor_config
}
}
}
""")
# # Query the API and get a JSON response
result = client.execute(query)
shots = result['all_shots']['shots']
df = pd.DataFrame(shots)
df.head()
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/graphql/type/directives.py:67, in GraphQLDirective.__init__(self, name, locations, args, is_repeatable, description, extensions, ast_node)
66 try:
---> 67 locations = tuple(
68 (
69 value
70 if isinstance(value, DirectiveLocation)
71 else DirectiveLocation[cast(str, value)]
72 )
73 for value in locations
74 )
75 except (KeyError, TypeError):
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/graphql/type/directives.py:71, in <genexpr>(.0)
66 try:
67 locations = tuple(
68 (
69 value
70 if isinstance(value, DirectiveLocation)
---> 71 else DirectiveLocation[cast(str, value)]
72 )
73 for value in locations
74 )
75 except (KeyError, TypeError):
File ~/.local/share/uv/python/cpython-3.12.6-linux-x86_64-gnu/lib/python3.12/enum.py:814, in EnumType.__getitem__(cls, name)
811 """
812 Return the member matching `name`.
813 """
--> 814 return cls._member_map_[name]
KeyError: 'DIRECTIVE_DEFINITION'
During handling of the above exception, another exception occurred:
TypeError Traceback (most recent call last)
Cell In[6], line 16
2 query = gql("""
3 query {
4 all_shots {
(...) 12 }
13 """)
15 # # Query the API and get a JSON response
---> 16 result = client.execute(query)
17 shots = result['all_shots']['shots']
18 df = pd.DataFrame(shots)
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/gql/client.py:483, in Client.execute(self, document, variable_values, operation_name, serialize_variables, parse_result, get_execution_result, **kwargs)
480 return data
482 else: # Sync transports
--> 483 return self.execute_sync(
484 document,
485 variable_values=variable_values,
486 operation_name=operation_name,
487 serialize_variables=serialize_variables,
488 parse_result=parse_result,
489 get_execution_result=get_execution_result,
490 **kwargs,
491 )
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/gql/client.py:246, in Client.execute_sync(self, document, variable_values, operation_name, serialize_variables, parse_result, get_execution_result, **kwargs)
235 def execute_sync(
236 self,
237 document: DocumentNode,
(...) 243 **kwargs,
244 ) -> Union[Dict[str, Any], ExecutionResult]:
245 """:meta private:"""
--> 246 with self as session:
247 return session.execute(
248 document,
249 variable_values=variable_values,
(...) 254 **kwargs,
255 )
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/gql/client.py:859, in Client.__enter__(self)
858 def __enter__(self):
--> 859 return self.connect_sync()
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/gql/client.py:840, in Client.connect_sync(self)
838 try:
839 if self.fetch_schema_from_transport and not self.schema:
--> 840 self.session.fetch_schema()
841 except Exception:
842 # we don't know what type of exception is thrown here because it
843 # depends on the underlying transport; we just make sure that the
844 # transport is closed and re-raise the exception
845 self.session.close()
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/gql/client.py:1296, in SyncClientSession.fetch_schema(self)
1291 introspection_query = get_introspection_query_ast(
1292 **self.client.introspection_args
1293 )
1294 execution_result = self.transport.execute(introspection_query)
-> 1296 self.client._build_schema_from_introspection(execution_result)
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/gql/client.py:191, in Client._build_schema_from_introspection(self, execution_result)
178 raise TransportQueryError(
179 (
180 "Error while fetching schema: "
(...) 187 extensions=execution_result.extensions,
188 )
190 self.introspection = cast(IntrospectionQuery, execution_result.data)
--> 191 self.schema = build_client_schema(self.introspection)
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/gql/utilities/build_client_schema.py:98, in build_client_schema(introspection)
95 if not any(directive["name"] == "include" for directive in directives):
96 directives.append(INCLUDE_DIRECTIVE_JSON)
---> 98 return build_client_schema_orig(introspection, assume_valid=False)
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/graphql/utilities/build_client_schema.py:402, in build_client_schema(introspection, assume_valid)
397 # Get the directives supported by Introspection, assuming empty-set if directives
398 # were not queried for.
399 directive_introspections = schema_introspection.get("directives")
400 directives = (
401 [
--> 402 build_directive(directive_introspection)
403 for directive_introspection in directive_introspections
404 ]
405 if directive_introspections
406 else []
407 )
409 # Then produce and return a Schema with these types.
410 return GraphQLSchema(
411 query=query_type,
412 mutation=mutation_type,
(...) 417 assume_valid=assume_valid,
418 )
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/graphql/utilities/build_client_schema.py:357, in build_client_schema.<locals>.build_directive(directive_introspection)
352 if directive_introspection.get("locations") is None:
353 raise TypeError(
354 "Introspection result missing directive locations:"
355 f" {inspect(directive_introspection)}."
356 )
--> 357 return GraphQLDirective(
358 name=directive_introspection["name"],
359 description=directive_introspection.get("description"),
360 is_repeatable=directive_introspection.get("isRepeatable", False),
361 locations=list(
362 cast(
363 Collection[DirectiveLocation],
364 directive_introspection.get("locations"),
365 )
366 ),
367 args=build_argument_def_map(directive_introspection["args"]),
368 )
File /srv/fair-mast/.docs-venv/lib/python3.12/site-packages/graphql/type/directives.py:76, in GraphQLDirective.__init__(self, name, locations, args, is_repeatable, description, extensions, ast_node)
67 locations = tuple(
68 (
69 value
(...) 73 for value in locations
74 )
75 except (KeyError, TypeError):
---> 76 raise TypeError(
77 f"{name} locations must be specified"
78 " as a collection of DirectiveLocation enum values."
79 )
80 if args is None:
81 args = {}
TypeError: deprecated locations must be specified as a collection of DirectiveLocation enum values.
Searching & Filtering Data#
We can also supply query parameters to GraphQL, such as limiting the number of returned values or filtering by value. Here we are limiting the first 3 values and we are selcting only shots from the M9 campaign.
# Write our GraphQL query.
query = gql("""
query ($campaign: String!) {
all_shots (limit: 3, where: {campaign: {eq: $campaign}}) {
shots {
shot_id
timestamp
preshot_description
divertor_config
}
}
}
""")
# Query the API and get a JSON response
result = client.execute(query, variable_values={"campaign": "M9"})
df = pd.DataFrame(result['all_shots']['shots'])
df.head()
Nested Queries#
One feature which makes GraphQL much more powerful than REST is that you may perform nested queries to gather different subsets of the data. For example, here we are going to query for all datasets with “AMC” in the name, and query for information about shots associated with this dataset.
# Write our GraphQL query.
query = gql("""
query ($signal: String!) {
all_signals (limit: 3, where: {name: {contains: $signal}}) {
signals {
name
url
shot {
shot_id
timestamp
divertor_config
}
}
}
}
""")
# Query the API and get a JSON response
client.execute(query, variable_values={"signal": "AMC"})
Pagination in GraphQL#
GraphQL queries are paginated. You may access other entries by including the page metadata and its associated elements. Here’s an example of getting paginated entries:
def do_query(cursor: str = None):
query = gql("""
query ($cursor: String) {
all_shots (limit: 3, where: {campaign: {contains: "M9"}}, cursor: $cursor) {
shots {
shot_id
timestamp
preshot_description
divertor_config
}
page_meta {
next_cursor
total_items
total_pages
}
}
}
""")
return client.execute(query, variable_values={'cursor': cursor})
def iterate_responses():
cursor = None
while True:
response = do_query(cursor)
yield response
cursor = response['all_shots']['page_meta']['next_cursor']
if cursor is None:
return
responses = iterate_responses()
print(next(responses))
print(next(responses))
print(next(responses))