Advanced topics#
Custom authentication#
The contents of execute()
's auth
parameter as passed to snug.execute()
.
This means that aside from basic authentication, a callable is accepted.
In most cases, you’re just going to be adding a header, for which a convenient shortcut exists:
>>> import snug
>>> my_auth = snug.header_adder({'Authorization': 'My auth header!'})
>>> execute(query, auth=my_auth)
...
See here for more detailed information.
HTTP Clients#
The client
parameter of execute()
allows the use
of different HTTP clients. By default, requests
and aiohttp are supported.
Example usage:
>>> import requests
>>> quiz.execute(query, client=requests.Session())
...
To register another HTTP client, see docs here.
Executors#
To make it easier to call execute()
repeatedly with specific arguments, the executor()
shortcut can be used.
>>> import requests
>>> exec = quiz.executor(auth=('me', 'password'),
... client=requests.Session())
>>> exec(some_query)
>>> exec(other_query)
...
>>> # we can still override arguments on each call
>>> exec(another_query, auth=('bob', 'hunter2'))
Response metadata#
Each result contains metadata about the HTTP response and request.
This can be accessed through the __metadata__
attribute,
which contains response
and request
>>> result = quiz.execute(...)
>>> meta = result.__metadata__
>>> meta.response
snug.Response(200, ...)
>>> meta.request
snug.Request('POST', ...)
Async#
execute_async()
is the asynchronous counterpart of
execute()
.
It has a similar API, but works with the whole async/await pattern.
Here is a simple example:
>>> import asyncio
>>> coro = quiz.execute_async(
... query,
... url='https://api.github.com/graphql',
... auth=('me', 'password'),
... )
>>> asyncio.run(coro)
...
The async HTTP client used by default is very rudimentary. Using aiohttp is highly recommended. Here is an example usage:
>>> import aiohttp
>>> async def mycode():
... async with aiohttp.ClientSession() as s:
... return await quiz.execute_async(
... query,
... url='https://api.github.com/graphql',
... auth=('me', 'password'),
... client=s,
... )
>>> asyncio.run(mycode())
...
Note
async_executor()
is also available
with a similar API as executor()
.
Caching schemas#
We’ve seen that Schema.from_url()
allows us to retrieve a schema directly from the API.
It is also possible to store a retrieved schema on the filesystem,
to avoid the need for downloading it every time.
This can be done with to_path()
.
>>> schema = quiz.Schema.from_url(...)
>>> schema.to_path('/path/to/schema.json')
Such a schema can be loaded with Schema.from_path()
:
>>> schema = quiz.Schema.from_path('/path/to/schema.json')
Populating modules#
As we’ve seen, a Schema
contains generated classes.
It can be useful to add these classes to a python module:
It allows pickling of instances
A python module is the idiomatic format for exposing classes.
In order to do this, provide the module
argument
in any of the schema constructors.
Then, use populate_module()
to add the classes
to this module.
# my_module.py
import quiz
schema = quiz.Schema.from_url(..., module=__name__)
schema.populate_module()
# my_code.py
import my_module
my_module.MyObject
See also
The examples show some practical applications of this feature.
Custom scalars#
GraphQL APIs often use custom scalars to represent data such as dates or URLs.
By default, custom scalars in the schema
are defined as GenericScalar
,
which accepts any of the base scalar types
(str
, bool
, float
, int
, ID
).
It is recommended to define scalars explicitly.
This can be done by implementing a Scalar
subclass
and specifying the __gql_dump__()
method
and/or the __gql_load__()
classmethod.
Below shows an example of a URI
scalar for GitHub’s v4 API:
import urllib
class URI(quiz.Scalar):
"""A URI string"""
def __init__(self, url: str):
self.components = urllib.parse.urlparse(url)
# needed if converting TO GraphQL
def __gql_dump__(self) -> str:
return self.components.geturl()
# needed if loading FROM GraphQL responses
@classmethod
def __gql_load__(cls, data: str) -> URI:
return cls(data)
To make sure this scalar is used in the schema, pass it to the schema constructor:
# this also works with Schema.from_url()
schema = quiz.Schema.from_path(..., scalars=[URI, MyOtherScalar, ...])
schema.URI is URI # True
The SELECTOR
API#
The quiz.SELECTOR
object allows writing GraphQL in python syntax.
It is recommended to import this object as an easy-to-type variable name,
such as _
.
import quiz.SELECTOR as _
Fields#
A selection with simple fields can be constructed by chaining attribute lookups. Below shows an example of a selection with 3 fields:
selection = _.field1.field2.foo
Note that we can write the same across multiple lines, using brackets.
selection = (
_
.field1
.field2
.foo
)
This makes the selection more readable. We will be using this style from now on.
How does this look in GraphQL? Let’s have a look:
>>> str(selection)
{
field1
field2
foo
}
Note
Newlines between brackets are valid python syntax. When chaining fields, do not add commas:
# THIS IS WRONG:
selection = (
_,
.field1,
.field2,
.foo,
)
Arguments#
To add arguments to a field, simply use python’s function call syntax with keyword arguments:
selection = (
_
.field1
.field2(my_arg=4, qux='my value')
.foo(bar=None)
)
This converts to the following GraphQL:
>>> str(selection)
{
field1
field2(my_arg: 4, qux: "my value")
foo(bar: null)
}
Selections#
To add a selection to a field, use python’s slicing syntax.
Within the []
brackets, a new selection can be defined.
selection = (
_
.field1
.field2[
_
.subfieldA
.subfieldB
.more[
_
.nested
.data
]
.another_field
]
.foo
)
This converts to the following GraphQL:
>>> str(selection)
{
field1
field2 {
subfieldA
subfieldB
more {
nested
data
}
another_field
}
foo
}
Aliases#
To add an alias to a field, add a function call before the field, specifying the field name:
selection = (
_
.field1
('my_alias').field2
.foo
)
This converts to the following GraphQL:
>>> str(selection)
{
field1
my_alias: field2
foo
}
Fragments & Directives#
Fragments and directives are not yet supported. See the roadmap.
Combinations#
The above features can be combined without restriction. Here is an example of a complex query to GitHub’s v4 API:
selection = (
_
.rateLimit[
_
.remaining
.resetAt
]
('hello_repo').repository(owner='octocat', name='hello-world')[
_
.createdAt
]
.organization(login='github')[
_
.location
.members(first=10)[
_.edges[
_.node[
_.id
]
]
('count').totalCount
]
]
)
This translates in to the following GraphQL:
>>> str(selection)
{
rateLimit {
remaining
resetAt
}
hello_repo: repository(owner: "octocat", name: "hello-world") {
createdAt
}
organization(login: "github") {
location
members(first: 10) {
edges {
node {
id
}
}
count: totalCount
}
}
}