Getting started with Python graphene

How to write GraphQL compliant apis with Python

Agenda

We will write an api endpoint which will respond to graphql queries.

  • We will use graphene library to create our GraphQL service.
  • We will expose this GraphQL service using Flask.
  • We will consume the api from a browser or angular/react client.

This post assumes basic familiarity with graphql.

Setup

Let’s assume our application works with Person entity. A Person has a first_name, last_name and age. Our graphql service will allow querying on Person.

Our queries would look like:

http://localhost:5000/?search={ person {firstName lastName age}} # Return all attributes of a single person.
http://localhost:5000/?search={ person {firstName lastName}} # Return firstName and lastName of a single person.
http://localhost:5000/?search={ people {firstName lastName}} # Return firstName and lastName of all people in our system.
http://localhost:5000/?search={ person(key: 1) {firstName lastName age}} # Return all attributes of a person identified by key 1.

In a real world application, we would fetch Persons from a database which would be converted to Python class by the ORM. We want to focus on GraphQL in this post and avoid database setup and interactions. So let’s create a namedtuple called Person and create few instances of it.

Let’s write our code in hello_graphene.py.

import collections

Person = collections.namedtuple("Person", ['first_name', 'last_name', 'age'])

data = {
    1: Person("steve", "jobs", 56),
    2: Person("bill", "gates", 63),
    3: Person("ken", "thompson", 76),
    4: Person("guido", "rossum", 63)
}

After this you should be able to do person.first_name, person.last_name and person.age. Let’s verify it from an ipython shell.

In [2]: from hello_graphene import data

In [3]: data
Out[3]:
{1: Person(first_name='steve', last_name='jobs', age=56),
 2: Person(first_name='bill', last_name='gates', age=63),
 3: Person(first_name='ken', last_name='thompson', age=76),
 4: Person(first_name='guido', last_name='rossum', age=63)}

In [4]: person = data[1]

In [5]: person.first_name
Out[5]: 'steve'

In [6]: person.last_name
Out[6]: 'jobs'

Creating schema

Ensure that graphene is installed.

pip install graphene

GraphQL expects a root type. We want to make person field available on root type.

We will have to write a Person type and the root type to accomplish this. This should be in hello_graphene.py.

from graphene import ObjectType, String, Int, Field

class PersonType(ObjectType):
    first_name = String()
    last_name = String()
    age = Int()

    def resolve_first_name(person, info):
        return person.first_name

    def resolve_last_name(person, info):
        return person.last_name

    def resolve_age(person, info):
        return person.age    

class Query(ObjectType):
    person = Field(PersonType)

    def resolve_person(root, info):
        return data[1]

Convention suggests that we name the root type class as Query.

Any GraphQL type we create must extend from graphene.ObjectType. GraphQL dictates that there must be a resolver function for each field on each type. That’s whey we have resolve_person for person, resolve_first_name for first_name and so on.

For now we have hardcoded the resolver for person to always return details for person with key 1. We are fixing it soon, hang on.

We need to tell to GraphQL service that the root type is Query. The mechanism to do that is to add a Schema instance.

from graphene import Schema
schema = Schema(query=Query)

Our full hello_graphene.py looks like:

import collections
from graphene import ObjectType, String, Schema, Int, Field

Person = collections.namedtuple("Person", ['first_name', 'last_name', 'age'])

data = {
    1: Person("steve", "jobs", 56),
    2: Person("bill", "gates", 63),
    3: Person("ken", "thompson", 76),
    4: Person("guido", "rossum", 63)
}

class PersonType(ObjectType):
    first_name = String()
    last_name = String()
    age = Int()

    def resolve_first_name(person, info):
        return person.first_name

    def resolve_last_name(person, info):
        return person.last_name

    def resolve_age(person, info):
        return person.age    

class Query(ObjectType):
    person = Field(PersonType)

    def resolve_person(root, info):
        return data[1]

schema = Schema(query=Query)

Our GraphQL service is ready now.

Executing queries

Let’s execute a GraphQL query from the shell.

In [3]: from hello_graphene import schema

In [7]: query = '{person {firstName lastName age} }'

In [8]: result = schema.execute(query)

In [9]: result.data
Out[9]:
OrderedDict([('person',
              OrderedDict([('firstName', 'steve'),
                           ('lastName', 'jobs'),
                           ('age', 56)]))])

Notice how the result datastructure has the same structure as the query.

Let’s execute one more query to ensure that the service only returns the requested fields.

In [10]: query = '{person {firstName} }'

In [11]: result = schema.execute(query)

In [12]: result.data
Out[12]: OrderedDict([('person', OrderedDict([('firstName', 'steve')]))])

We want to expose the service on an endpoint so that browser or any client can consume the service. Let’s expose a Flask endpoint.

# flask_graphql.py

import json
from flask import Flask, request

from hello_graphene import schema

app = Flask(__name__)

@app.route('/graphql')
def graphql():
    query = request.args.get('query')
    result = schema.execute(query)
    d = json.dumps(result.data)
    return '{}'.format(d)

Start the flask server.

$ export FLASK_APP=flask_graphql.py
$ flask run

Let’s make a request to flask app with a GraphQL query.

Let’s modify the GraphQL service to allow getting details of any person. This requires using GraphQL arguments.

We need to allow arguments on person field. Modify person to look like:

person = Field(PersonType, key=Int())

We will have to modify resolver for person to accomodate the argument too.

def resolve_person(root, info, key):
    return data[key]

Restart the shell and get data for steve and bill.

In [1]: from hello_graphene import schema

In [2]: query = '{person(key: 1) {firstName} }'

In [3]: schema.execute(query).data
Out[3]: OrderedDict([('person', OrderedDict([('firstName', 'steve')]))])

In [4]: query = '{person(key: 2) {firstName} }'

In [5]: schema.execute(query).data
Out[5]: OrderedDict([('person', OrderedDict([('firstName', 'bill')]))])

We could have named the argument anything. We could have named it identifier instead of key.

person = Field(PersonType, identifier=Int())

def resolve_person(root, info, identifier):
    return data[identifier]

Let’s use the api from browser and get data for bill.

Ideally the api would be consumed from a mobile client or from a single page application.

If person is retrieved from a database using SQLAlchemy, then the argument would probably be named id and the resolver would look something like:

person = Field(PersonType, id=Int())

def resolve_person(root, info, id):
    return Person.query.get(id)

Fetching a list of people

We want our service to return details of all people in our system. Let’s add a field called people on the root type.

from graphene import List

class Query(ObjectType):
    person = Field(PersonType, key=Int())
    people = List(PersonType)

    def resolve_person(root, info, key):
        return data[key]

    def resolve_people(root, info):
        return data.values()

Since people field provides a list of people, so we set it’s type as graphene.List. Each entry of the list would be a PersonType.

In [1]: from hello_graphene import schema

In [2]: query = '{people {firstName age} }'

In [3]: schema.execute(query).data
Out[3]:
OrderedDict([('people',
              [OrderedDict([('firstName', 'steve'), ('age', 56)]),
               OrderedDict([('firstName', 'bill'), ('age', 63)]),
               OrderedDict([('firstName', 'ken'), ('age', 76)]),
               OrderedDict([('firstName', 'guido'), ('age', 63)])])])

We can get all people from a client by calling localhost:5000/?query={ people {firstName lastName age} }.

Supporting defaults

Currently we cannot query on person without key. Let’s try a query which would cause an exception.

In [6]: query = '{person {firstName} }'

In [7]: schema.execute(query).data
TypeError: resolve_person() missing 1 required positional argument: 'key'
Traceback (most recent call last):
  File "/Users/akshar/Envs/gryffindor/lib/python3.6/site-packages/graphql/execution/executor.py", line 450, in resolve_or_error
    return executor.execute(resolve_fn, source, info, **args)
  File "/Users/akshar/Envs/gryffindor/lib/python3.6/site-packages/graphql/execution/executors/sync.py", line 16, in execute
    return fn(*args, **kwargs)
graphql.error.located_error.GraphQLLocatedError: resolve_person() missing 1 required positional argument: 'key'

If person’s key isn’t provided, then we want to respond with steve’s details. We can accomplish this by setting a default_value on person and this default_value should contain steve’s key.

person = Field(PersonType, key=Int(default_value=1))

Restart the shell and try the query once more.

In [4]: query = '{person {firstName} }'

In [5]: schema.execute(query).data
Out[5]: OrderedDict([('person', OrderedDict([('firstName', 'steve')]))])

If we pass a key though, then corresponding person’s details would be fetched.

In [6]: query = '{person(key: 2) {firstName} }'

In [7]: schema.execute(query).data
Out[7]: OrderedDict([('person', OrderedDict([('firstName', 'bill')]))])

Hope this post was helpful. Stay tuned for more GraphQL posts.

Thank you for reading the Agiliq blog. This article was written by Akshar on Aug 28, 2019 in GraphQLAPIpythongraphene .

You can subscribe ⚛ to our blog.

We love building amazing apps for web and mobile for our clients. If you are looking for development help, contact us today ✉.

Would you like to download 10+ free Django and Python books? Get them here