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
}
}
}