Accueil / Blog / Métier / Archive du blog de l'année 2013 / Multi-format RESTful APIs with Cornice

Multi-format RESTful APIs with Cornice

Par Alex Marandon 15/11/2013
How to create a RESTful web API that support writing and reading multiple formats using Cornice and Pyramid

Cornice is a library from the Mozilla folks that makes it really easy to create RESTful web services in Python, using the Pyramid web framework. Cornice was able to send responses in multiple formats using content negotiation based on the Accept header. Recently Makina Corpus made a contribution that unlocked the potential of creating APIs that can also receive requests in multiple formats.

Cornice comes with a Pyramid template that allows to create a project directory using Cornice in one command:

$ pcreate -t cornice myapp

In the freshly created project directory, we'll find a file myapp/views.py, which already contains a really simple service that returns a JSON response:

hello = Service(name='hello', path='/', description="Simplest app")

@hello.get()
def get_info(request):
    """Returns Hello in JSON."""
    return {'Hello': 'World'}

Let's start a development server (you might need to install waitress first):

$ python setup.py develop
$ pserve --reload myapp.ini

And check that we can send a request and get a response back:

$ curl -D - http://0.0.0.0:6543
HTTP/1.1 200 OK
Content-Length: 18
Content-Type: application/json; charset=UTF-8
Date: Fri, 15 Nov 2013 10:39:41 GMT
Server: waitress

{"Hello": "World"}

Now let's say we'd like our API to send a response in plain text. We can use content negotiation with the Accept header and create a Pyramid renderer that will format a plain text response. First we create a plain text renderer. It can go in myapp/__init__.py:

class TextRenderer(object):

    def __init__(self, info):
        pass

    def __call__(self, value, system):
        request = system.get('request')
        if request is not None:
            response = request.response
            response.content_type = 'text/plain'
        name = value['Hello']
        return u"Hello, {}!".format(name)

We need to register this new renderer in our app main function:

def main(global_config, **settings):
    config = Configurator(settings=settings)
    config.include("cornice")
    config.scan("myapp.views")
    config.add_renderer('text', TextRenderer)
    return config.make_wsgi_app()

Now we update our service so that it uses the correct renderer based on the Accept header:

@hello.get(accept='text/plain', renderer='text')
@hello.get(accept='application/json', renderer='json')
def get_info(request):
    """Returns Hello in JSON."""
    return {'Hello': 'World'}

Let's check that it works:

$ curl -D - -H 'Accept: text/plain' http://0.0.0.0:6543
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain; charset=UTF-8
Date: Fri, 15 Nov 2013 11:22:41 GMT
Server: waitress

Hello, World!


$ curl -D - -H 'Accept: application/json' http://0.0.0.0:6543
HTTP/1.1 200 OK
Content-Length: 18
Content-Type: application/json; charset=UTF-8
Date: Fri, 15 Nov 2013 11:22:48 GMT
Server: waitress

{"Hello": "World"}

This is fine but now we'd like to produce a response based on user input. We're going to allow our users to provide their name in plain text or in JSON. We also want to validate user input to make sure the name we receive is at least two characters long. Cornice integrates well with the Colander validation library Make sure you have at least colander 1.0b1 installed.

Let's create a colander schema to implement our validation rule:

import colander

class NameSchema(colander.MappingSchema):
    name = colander.SchemaNode(colander.String(),
                               validator=colander.Length(2))

Then we write a POST request handler that is wired up with our Colander schema:

@hello.post(accept='text/plain', renderer='text', schema=NameSchema)
@hello.post(accept='application/json', renderer='json', schema=NameSchema)
def post_info(request):
    """Returns Hello in JSON."""
    name = request.validated['name']
    return {'Hello': name}

Let's see how this works if I post JSON containing my name:

$ curl -H 'Accept: application/json' http://0.0.0.0:6543 -d '{"name": "Alex"}'
{"status": "error", "errors": [{"location": "body", "name": "name", "description": "name is missing"}]}

This isn't working because we haven't told the server what format we were using. If we don't specify the format, Cornice will assume we're sending application/x-www-form-urlencoded, which is the format sent by web browsers when we submit a form. So this will work:

$ curl -H 'Accept: application/json' http://0.0.0.0:6543 -d 'name=Alex'
{"Hello": "Alex"}

To get Cornice to validate JSON input properly, we need to specify the Content-Type header:

$ curl -H 'Accept: application/json' -H 'Content-Type: application/json' http://0.0.0.0:6543 -d '{"name": "Alex"}'
{"Hello": "Alex"}

By the way, does our validation rule work as expected?

$ curl -H 'Accept: application/json' -H 'Content-Type: application/json' http://0.0.0.0:6543 -d '{"name": "A"}'
{"status": "error", "errors": [{"location": "body", "name": "name", "description": "Shorter than minimum length 2"}]}

Perfect! But remember that we'd also like to be able to send our name in plain text as the request body, without any structured format around it:

$ curl -H 'Accept: application/json' -H 'Content-Type: text/plain' http://0.0.0.0:6543 -d 'Alex'
{"status": "error", "errors": [{"location": "body", "name": "name", "description": "name is missing"}]}

This is not working because out of the box Cornice can only validate JSON and form-urlencoded data. To tell our app how to interpret plain text, we need to add a deserializer. A deserializer is just a callable taking a request argument and returning a Python data structure that Colander can understand (a cstruct). In this case the callable simply returns a dictionary containing a single entry name whose value is the request body:

def plain_text_deserializer(request):
    # Here we could be parsing XML or any complex format as long as we're
    # returning a dictionary with the same structure
    return {'name': request.body}

And we assign our new deserializer to our POST handler:

@hello.post(accept='text/plain', renderer='text', schema=NameSchema,
            deserializer=plain_text_deserializer)
@hello.post(accept='application/json', renderer='json', schema=NameSchema)
def post_info(request):
    """Returns Hello in JSON."""
    name = request.validated['name']
    return {'Hello': name}

And now we can post plain text:

$ curl -H 'Accept: text/plain' -H 'Content-Type: text/plain' http://0.0.0.0:6543 -d 'Al'
Hello, Al!

So this is basically how we implement read/write multi-format APIs in Cornice with Colander validation: we make sure we have appropriate renderers and deserializers for each format and we plug them into our view configuration. Of course in any real-world scenario your renderers and deserializers will be much more complex.

Thank you for reading so far and special thanks to Alexis Métaireau for merging our work on deserializers.

Mots-clés associés : ,
Derniers articles
Butinage n°40Butinage n°40 20/08/2014 Quarantième exemplaire de la veille régulière réalisée par Makina Corpus sur l'actualité web. Simon Georges
Formation PostgreSQL / PostGIS du 23 au 25 septembre à Toulouse Formation PostgreSQL / PostGIS du 23 au 25 septembre à Toulouse 10/08/2014 Vous êtes technicien, responsable SIG ou même développeur ? Venez suivre la formation base de données PostgreSQL / PostGIS à Toulouse du 23 au 25 septembre. Anael Boulier
Comment tester un site sur un domaine local depuis un iPadComment tester un site sur un domaine local depuis un iPad 08/08/2014 Il est parfois obligatoire de passer par un nom de domaine pour tester un site web. Ce nom de domaine peut facilement être ajouté au fichier /etc/hosts pour tester localement. Il n'y a malheureusement pas de solutions similaires pour l'iPad, il faut donc en trouver une autre. Yann Fouillat
Formation TileMill le 8 octobre à ToulouseFormation TileMill le 8 octobre à Toulouse 05/08/2014 Profitez d'une journée pour apprendre à utiliser, combiner et styler vos données géographiques pour créer une carte interactive riche. Anael Boulier