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